diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e01ad14..edb273891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - [commands] Add support for named command arguments - [common] Add `CollectionHelpers#putAllIfAbsent(Map, Map)` to add all elements from one map to another if key isn't present - [commands] Improve code structure for registering a command +- [commands] Add Compound parser +- [commands] Add `ParserAssertions#assertParserTabCompletion` for better unit testing ## 0.0.36 - [common] Improve MinecraftVersion added known version (1.8 to 1.21.1) with protocol number and some helper methods @@ -40,6 +42,7 @@ - [spigot-commands] `MaterialParser` using namespaced key for tab competition - [spigot] Fix visibility API to work on 1.8 - [commands] Add `ValueRequirement` +- [docs] Add docs about region box, transform, region and rotation. ## v0.0.29 - [commands] Improve location parser diff --git a/Writerside/cb.tree b/Writerside/cb.tree index 44e72c7bc..ae38603ee 100644 --- a/Writerside/cb.tree +++ b/Writerside/cb.tree @@ -21,6 +21,7 @@ + diff --git a/Writerside/topics/Examples.md b/Writerside/topics/Examples.md index 332552e62..2b762199c 100644 --- a/Writerside/topics/Examples.md +++ b/Writerside/topics/Examples.md @@ -14,4 +14,5 @@ You can find some examples of using the commands system in this page * [`Write information about command`](Command-Information.md) * [`Dynamic annotation parser`](Dynamic-annotation-parser.md) * [`Value Requirement`](value-requirement.md) -* [`Named command arguments`](parameter-name.md) \ No newline at end of file +* [`Named command arguments`](parameter-name.md) +* [`Compound parser`](compound-parser.md) \ No newline at end of file diff --git a/Writerside/topics/compound-parser.md b/Writerside/topics/compound-parser.md new file mode 100644 index 000000000..ae0b2e3bb --- /dev/null +++ b/Writerside/topics/compound-parser.md @@ -0,0 +1,142 @@ +# 🔬 Compound parser + + +Available Since 0.0.37 + + +**Table of content:** +- [Introduction](#introduction) +- [Usage](#usage) +- [Advantages](#advantages) + +## Introduction +Writing custom parsers for complicated arguments, such as locations, can be a tedious and error prone task. +For this reason we have added a way to define compound parsers in the same way its possible to define commands. Each parser could have multiple variants with different input types. + +## Usage +We will create a simple compound parser called `LocationParser` that will parse a location from a string doubles, +We will also have a variant with yaw & pitch as float + + + + + +```java +// We need to provide all of our parsers that we will want to use +@WithParser(DoubleParser.class) +@WithParser(FloatParser.class) +@WithParser(WorldParser.class) +public class LocationParser extends CompoundParser { + + public static final String DEFAULT_KEYWORD = "location"; + + public LocationParser(int priority, String keyword) { + super( + keyword, // We need to provide a keyword + Location.class, // We need to provide a type of the result + priority, // We need to provide a priority of the parser + new SimpleArgumentMapper(), // argument mapper to map arguments + new SimpleCommandLexer()// command lexer to tokenize the variant + ); + } + + public LocationParser(int priority) { + this(priority, DEFAULT_KEYWORD); + } + + // Simple variant + @ParserVariant(" ") + public Location parseWorldWithXyz(World world, double x, double y, double z) { + return new Location(world, x, y, z); + } + + // another variant with yaw & pitch + @ParserVariant(" ") + public Location parseWorldWithXyzYawPitch(World world, double x, double y, double z, float yaw, float pitch) { + return new Location(world, x, y, z, yaw, pitch); + } + + // We could also have a requirement + @SenderLimit(SenderType.PLAYER) + @ParserVariant(" ") + public Location parseWithXyz(Player sender, double x, double y, double z) { + return new Location( + sender.getWorld(), + x, + y, + z + ); + } + + @SenderLimit(SenderType.PLAYER) + @ParserVariant(" ") + public Location parseWithXyzYawPitch(Player sender, double x, double y, double z, float yaw, float pitch) { + return new Location( + sender.getWorld(), + x, + y, + z, + yaw, + pitch + ); + } +} +``` + + + + + +That is everything you need to do to create your own compound parser. +There are a couple of things to note: + - You need to provide all of your parsers that you will want to use + - You may want to create a wrapper class that will wrap your compound parser and will cache the variants so you don't have to create them every time someone want to change keyword/priority + +#### How could we warp it? + +We will change `LocationParser` to be called `LocationParserImpl` and put it as `/* package-private */` then we will create class `LocationParser` that will be public +```java +public class LocationParser extends ArgumentParser { + + private static LocationParserImpl impl = new LocationParserImpl(); + + public static final String DEFAULT_KEYWORD = "location"; + + public LocationParser(int priority, String keyword) { + super( + keyword, + Location.class, + priority + ); + } + + public LocationParser(int priority) { + this(priority, DEFAULT_KEYWORD); + } + + @Override + public Optional> parse(CommandProcessingContext processingContext) { + return impl.parse(processingContext); + } + + @Override + public OptionalInt tryParse(CommandProcessingContext processingContext) { + return impl.tryParse(processingContext); + } + + @Override + public Optional tabCompletion(CommandProcessingContext processingContext) { + return impl.tabCompletion(processingContext); + } + +} +``` +That will save you memory and computation time but the parsers you use must support multithreading because there could be multiple threads running at the same time, and you don't want to create a new instance of the parser every time. +You could lock the parser but it's not recommended because it will slow down the parsing process. + +## Advantages + - **Improved Readability** 📖: The compound parser provides a clear and descriptive name for each variant, making the command structure more readable and self-explanatory. +- **Easier to Use** 👍: The compound parser is easy to use and can be used to create complex parsers with multiple parameters and variants with different requirements. +- **Reduced Code** 📦: The compound parser is a reusable component that can be used in multiple commands to avoid code duplication and reduce the amount of code needed to write a command. +- **Increased Flexibility** 🎯: The compound parser allows you to create parsers that are more complex and can be used in multiple places in your command structure. +- **Reduced Error Prone** 🐛: The compound parser is easy to use and can be used to create complex parsers with multiple parameters and variants with different requirements. diff --git a/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/PermissionFactory.java b/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/PermissionFactory.java index 4fef83738..b3d6f5100 100644 --- a/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/PermissionFactory.java +++ b/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/PermissionFactory.java @@ -10,7 +10,7 @@ package net.apartium.cocoabeans.commands.spigot.requirements.factory; -import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.commands.Sender; import net.apartium.cocoabeans.commands.requirements.*; import net.apartium.cocoabeans.commands.spigot.exception.PermissionException; @@ -22,7 +22,7 @@ public class PermissionFactory implements RequirementFactory { @Nullable @Override - public Requirement getRequirement(CommandNode node, Object obj) { + public Requirement getRequirement(GenericNode node, Object obj) { if (!(obj instanceof Permission permission)) return null; @@ -33,13 +33,18 @@ private record PermissionImpl(Permission permission, String permissionAsString, @Override public RequirementResult meetsRequirement(RequirementEvaluationContext context) { + if (invert) + return meetsInvertedRequirement(context); + Sender sender = context.sender(); - if (sender == null || !(sender.getSender() instanceof CommandSender commandSender)) + + if ((sender.getSender() == null || !(sender.getSender() instanceof CommandSender commandSender))) { return RequirementResult.error(new UnmetPermissionResponse( this, context, - "You don't have permission to execute this command" + "You must be a command sender" )); + } if (!commandSender.hasPermission(permissionAsString)) return RequirementResult.error(new UnmetPermissionResponse( @@ -51,6 +56,26 @@ public RequirementResult meetsRequirement(RequirementEvaluationContext context) return RequirementResult.meet(); } + private RequirementResult meetsInvertedRequirement(RequirementEvaluationContext context) { + Sender sender = context.sender(); + + if (sender.getSender() == null || !(sender.getSender() instanceof CommandSender commandSender)) + return RequirementResult.error(new UnmetPermissionResponse( + this, + context, + "You must be a command sender" + )); + + if (commandSender.hasPermission(permissionAsString)) + return RequirementResult.error(new UnmetPermissionResponse( + this, + context, + "You should not have permission to execute this command" + )); + + return RequirementResult.meet(); + } + private class UnmetPermissionResponse extends UnmetRequirementResponse { diff --git a/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/SenderLimitFactory.java b/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/SenderLimitFactory.java index 0882d1eb4..c985180cb 100644 --- a/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/SenderLimitFactory.java +++ b/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/SenderLimitFactory.java @@ -12,6 +12,7 @@ import net.apartium.cocoabeans.CollectionHelpers; import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.commands.requirements.*; import net.apartium.cocoabeans.commands.spigot.SenderType; import net.apartium.cocoabeans.commands.spigot.exception.SenderLimitException; @@ -30,7 +31,7 @@ public class SenderLimitFactory implements RequirementFactory { @Nullable @Override - public Requirement getRequirement(CommandNode node, Object obj) { + public Requirement getRequirement(GenericNode node, Object obj) { if (!(obj instanceof SenderLimit senderLimit)) return null; diff --git a/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/WhitelistRequirementFactory.java b/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/WhitelistRequirementFactory.java index de01a18f6..ede91821f 100644 --- a/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/WhitelistRequirementFactory.java +++ b/commands/spigot-platform/src/main/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/WhitelistRequirementFactory.java @@ -11,6 +11,7 @@ package net.apartium.cocoabeans.commands.spigot.requirements.factory; import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.commands.requirements.*; import net.apartium.cocoabeans.commands.spigot.exception.WhitelistException; import net.apartium.cocoabeans.commands.spigot.requirements.Whitelist; @@ -30,7 +31,7 @@ public class WhitelistRequirementFactory implements RequirementFactory { @Nullable @Override - public Requirement getRequirement(CommandNode node, Object obj) { + public Requirement getRequirement(GenericNode node, Object obj) { if (!(obj instanceof Whitelist whitelist)) return null; diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/game/commands/utils/InGameFactory.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/game/commands/utils/InGameFactory.java index 857abe01a..593574e8f 100644 --- a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/game/commands/utils/InGameFactory.java +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/game/commands/utils/InGameFactory.java @@ -1,6 +1,7 @@ package net.apartium.cocoabeans.commands.spigot.game.commands.utils; import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.commands.requirements.*; import net.apartium.cocoabeans.commands.spigot.game.Game; import net.apartium.cocoabeans.commands.spigot.game.GamePlayer; @@ -13,7 +14,7 @@ public class InGameFactory implements RequirementFactory { @Override - public @Nullable Requirement getRequirement(CommandNode commandNode, Object obj) { + public @Nullable Requirement getRequirement(GenericNode node, Object obj) { if (!(obj instanceof InGame inGame)) return null; diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/CatCommand.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/CatCommand.java new file mode 100644 index 000000000..7385fc1eb --- /dev/null +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/CatCommand.java @@ -0,0 +1,16 @@ +package net.apartium.cocoabeans.commands.spigot.parsers; + +import net.apartium.cocoabeans.commands.Command; +import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.SubCommand; +import org.bukkit.command.CommandSender; + +@Command("cat") +public class CatCommand implements CommandNode { + + @SubCommand("set ") + public void setCat(CommandSender sender, String id, Meow cat) { + sender.sendMessage(id + " has been set to " + cat); + } + +} diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/Meow.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/Meow.java new file mode 100644 index 000000000..a11573abe --- /dev/null +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/Meow.java @@ -0,0 +1,13 @@ +package net.apartium.cocoabeans.commands.spigot.parsers; + +public record Meow(String cat, int age, Gender gender) { + + public enum Gender { + MALE, FEMALE, OTHER + } + + @Override + public String toString() { + return "cat: " + cat + ", age: " + age + ", gender: " + gender; + } +} diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/MeowCommandTest.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/MeowCommandTest.java new file mode 100644 index 000000000..d89c7ae76 --- /dev/null +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/MeowCommandTest.java @@ -0,0 +1,31 @@ +package net.apartium.cocoabeans.commands.spigot.parsers; + +import be.seeseemelk.mockbukkit.entity.PlayerMock; +import net.apartium.cocoabeans.commands.spigot.CommandsSpigotTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MeowCommandTest extends CommandsSpigotTestBase { + + private PlayerMock player; + + @Override + @BeforeEach + public void setup() { + super.setup(); + commandManager.registerArgumentTypeHandler(new MeowParser(0)); + commandManager.addCommand(new CatCommand()); + + player = server.addPlayer("ikfir"); + } + + @Test + void setCommand() { + execute(player, "cat", "set tom a_cat 13 male"); + assertEquals("tom has been set to cat: a_cat, age: 13, gender: MALE", player.nextMessage()); + } + + +} diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/MeowParser.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/MeowParser.java new file mode 100644 index 000000000..d2b6fe51a --- /dev/null +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/parsers/MeowParser.java @@ -0,0 +1,39 @@ +package net.apartium.cocoabeans.commands.spigot.parsers; + +import net.apartium.cocoabeans.commands.SimpleArgumentMapper; +import net.apartium.cocoabeans.commands.lexer.SimpleCommandLexer; +import net.apartium.cocoabeans.commands.parsers.*; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +@WithParser(StringParser.class) +@WithParser(IntParser.class) +public class MeowParser extends CompoundParser { + + + public MeowParser(int priority) { + super("meow", Meow.class, priority, new SimpleArgumentMapper(), new SimpleCommandLexer()); + } + + @ParserVariant(" ") + public Meow serialize(String cat, int age, Meow.Gender gender) { + return new Meow(cat, age, gender); + } + + + @SourceParser( + keyword = "gender", + clazz = Meow.Gender.class, + resultMaxAgeInMills = -1 + ) + public Map getGenders() { + return Arrays.stream(Meow.Gender.values()) + .collect(Collectors.toMap( + value -> value.name().toLowerCase(), + value -> value + )); + } + +} diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/PermissionCommand.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/PermissionCommand.java new file mode 100644 index 000000000..fa331b7bc --- /dev/null +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/PermissionCommand.java @@ -0,0 +1,37 @@ +package net.apartium.cocoabeans.commands.spigot.requirements; + +import net.apartium.cocoabeans.commands.Command; +import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.SubCommand; +import net.apartium.cocoabeans.commands.exception.ExceptionHandle; +import net.apartium.cocoabeans.commands.spigot.exception.PermissionException; +import org.bukkit.command.CommandSender; + +@Command("permission") +public class PermissionCommand implements CommandNode { + + @Permission("my.test") + @SubCommand("test") + public void testWithPermission(CommandSender sender) { + sender.sendMessage("You got a permission"); + } + + @Permission(value = "my.test", invert = true) + @SubCommand("test") + public void testWithoutPermission(CommandSender sender) { + sender.sendMessage("You don't have a permission"); + } + + @Permission(value = "my.test", invert = true) + @SubCommand("wow") + public void testWithAndWithoutPermission(CommandSender sender) { + sender.sendMessage("nah"); + } + + @ExceptionHandle(PermissionException.class) + public void noPermission(CommandSender sender, PermissionException permissionException) { + sender.sendMessage(permissionException.getMessage()); + } + + +} diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/PermissionCommandTest.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/PermissionCommandTest.java new file mode 100644 index 000000000..5bf7630f1 --- /dev/null +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/PermissionCommandTest.java @@ -0,0 +1,71 @@ +package net.apartium.cocoabeans.commands.spigot.requirements; + +import be.seeseemelk.mockbukkit.entity.PlayerMock; +import net.apartium.cocoabeans.commands.spigot.CommandsSpigotTestBase; +import org.bukkit.entity.Player; +import org.bukkit.permissions.PermissionAttachment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class PermissionCommandTest extends CommandsSpigotTestBase { + + PlayerMock player; + + @BeforeEach + @Override + public void setup() { + super.setup(); + + commandManager.addCommand(new PermissionCommand()); + + player = server.addPlayer("ikfir"); + } + + @Test + void testAddPermission() { + assertFalse(player.hasPermission("my.test")); + addPermission(player, "my.test"); + assertTrue(player.hasPermission("my.test")); + } + + @Test + void testWithPermission() { + addPermission(player, "my.test"); + execute(player, "permission", "test"); + assertEquals("You got a permission", player.nextMessage()); + } + + @Test + void testWithoutPermission() { + execute(player, "permission", "test"); + assertEquals("You don't have a permission", player.nextMessage()); + } + + @Test + void testWithAndWithoutPermission() { + execute(player, "permission", "test"); + assertEquals("You don't have a permission", player.nextMessage()); + + addPermission(player, "my.test"); + execute(player, "permission", "test"); + assertEquals("You got a permission", player.nextMessage()); + } + + @Test + void wowTestWithAndWithoutPermission() { + execute(player, "permission", "wow"); + assertEquals("nah", player.nextMessage()); + + addPermission(player, "my.test"); + execute(player, "permission", "wow"); + assertEquals("You should not have permission to execute this command", player.nextMessage()); + } + + void addPermission(Player target, String permission) { + PermissionAttachment attachment = target.addAttachment(plugin); + attachment.setPermission(permission, true); + } + +} diff --git a/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/PermissionFactoryTest.java b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/PermissionFactoryTest.java new file mode 100644 index 000000000..0fce63d23 --- /dev/null +++ b/commands/spigot-platform/src/test/java/net/apartium/cocoabeans/commands/spigot/requirements/factory/PermissionFactoryTest.java @@ -0,0 +1,16 @@ +package net.apartium.cocoabeans.commands.spigot.requirements.factory; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNull; + +class PermissionFactoryTest { + + @Test + void passingPermissionFactory() { + PermissionFactory permissionFactory = new PermissionFactory(); + + assertNull(permissionFactory.getRequirement(null, null)); + } + +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/ArgumentMapper.java b/commands/src/main/java/net/apartium/cocoabeans/commands/ArgumentMapper.java index cc259b263..a23893ac4 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/ArgumentMapper.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/ArgumentMapper.java @@ -10,7 +10,6 @@ package net.apartium.cocoabeans.commands; -import net.apartium.cocoabeans.commands.parsers.ArgumentParser; import net.apartium.cocoabeans.commands.requirements.Requirement; import java.util.List; @@ -28,6 +27,6 @@ public interface ArgumentMapper { * @param requirements requirements * @return list of argument indexes */ - List> mapIndices(RegisteredCommandVariant.Parameter[] parameters, List> argumentParsers, List requirements); + List> mapIndices(RegisteredVariant.Parameter[] parameters, List> argumentParsers, List requirements); } diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandBranchProcessor.java b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandBranchProcessor.java index 480bd67c8..ff5980782 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandBranchProcessor.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandBranchProcessor.java @@ -116,8 +116,8 @@ private void addToParserArgs(RequirementResult requirementResult, CommandContext context) { for (RequirementResult.Value value : requirementResult.getValues()) { context.parsedArgs() - .computeIfAbsent(value.clazz(), (clazz) -> new ArrayList<>()) - .add(0, value.value()); + .computeIfAbsent(value.clazz(), clazz -> new ArrayList<>()) + .add(value.value()); } } diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandManager.java b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandManager.java index e21f8c3d0..8206fe75c 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandManager.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandManager.java @@ -89,35 +89,9 @@ public boolean handle(Sender sender, String commandName, String[] args) throws T 0 ); - if (context == null) { - BadCommandResponse badCommandResponse = null; - for (RegisteredCommand.RegisteredCommandNode listener : registeredCommand.getCommands()) { - - RequirementResult requirementResult = listener.requirements().meetsRequirements(new RequirementEvaluationContext(sender, commandName, args, 0)); - if (requirementResult.hasError()) { - badCommandResponse = requirementResult.getError(); - break; - } - } - + if (context == null) + return handleNullContext(sender, commandName, args, registeredCommand); - // fall back will be called even if sender doesn't meet requirements - for (RegisteredCommand.RegisteredCommandNode listener : registeredCommand.getCommands()) { - if (listener.listener().fallbackHandle(sender, commandName, args)) - return true; - - } - - if (badCommandResponse != null) { - if (handleError(null, sender, commandName, args, registeredCommand, context.error().getError())) - return true; - - context.error().throwError(); - return false; // should never reach here - } - - return false; - } if (context.hasError()) { if (handleError(context, sender, commandName, args, registeredCommand, context.error().getError())) @@ -128,23 +102,57 @@ public boolean handle(Sender sender, String commandName, String[] args) throws T } - for (RegisteredCommandVariant method : context.option().getRegisteredCommandVariants()) { + for (RegisteredVariant method : context.option().getRegisteredCommandVariants()) { try { if (invoke(context, sender, method)) return true; - } catch (Throwable e) { - if (handleError(context, sender, commandName, args, registeredCommand, e)) return true; + } catch (Exception e) { + if (handleError(context, sender, commandName, args, registeredCommand, e)) + return true; throw e; } } + return handleFallback(sender, commandName, args, registeredCommand); + } + + private boolean handleNullContext(Sender sender, String commandName, String[] args, RegisteredCommand registeredCommand) throws Exception { + BadCommandResponse badCommandResponse = null; + for (RegisteredCommand.RegisteredCommandNode listener : registeredCommand.getCommands()) { + + RequirementResult requirementResult = listener.requirements().meetsRequirements(new RequirementEvaluationContext(sender, commandName, args, 0)); + if (requirementResult.hasError()) { + badCommandResponse = requirementResult.getError(); + break; + } + } + + + // fall back will be called even if sender doesn't meet requirements for (RegisteredCommand.RegisteredCommandNode listener : registeredCommand.getCommands()) { if (listener.listener().fallbackHandle(sender, commandName, args)) return true; } + if (badCommandResponse != null) { + if (handleError(null, sender, commandName, args, registeredCommand, badCommandResponse.getError())) + return true; + + badCommandResponse.throwError(); + return false; // should never reach here + } + + return false; + } + + private boolean handleFallback(Sender sender, String commandName, String[] args, RegisteredCommand registeredCommand) { + for (RegisteredCommand.RegisteredCommandNode listener : registeredCommand.getCommands()) { + if (listener.listener().fallbackHandle(sender, commandName, args)) + return true; + } + return false; } @@ -186,16 +194,16 @@ private boolean invokeException(HandleExceptionVariant handleExceptionVariant, C return true; } - private boolean invoke(CommandContext context, Sender sender, RegisteredCommandVariant registeredCommandVariant) { - List parameters = new ArrayList<>(registeredCommandVariant.argumentIndexList().stream() + private boolean invoke(CommandContext context, Sender sender, RegisteredVariant registeredVariant) { + List parameters = new ArrayList<>(registeredVariant.argumentIndexList().stream() .map((argumentIndex -> argumentIndex.get(context.toArgumentContext()))) .toList()); - parameters.add(0, registeredCommandVariant.commandNode()); + parameters.add(0, registeredVariant.node()); - for (int i = 0; i < registeredCommandVariant.parameters().length; i++) { + for (int i = 0; i < registeredVariant.parameters().length; i++) { Object obj = parameters.get(i + 1); // first element is class instance - for (ArgumentRequirement argumentRequirement : registeredCommandVariant.parameters()[i].argumentRequirements()) { + for (ArgumentRequirement argumentRequirement : registeredVariant.parameters()[i].argumentRequirements()) { if (!argumentRequirement.meetsRequirement(sender, context, obj)) return false; } @@ -203,7 +211,7 @@ private boolean invoke(CommandContext context, Sender sender, RegisteredCommandV Object output; try { - output = registeredCommandVariant.method().invokeWithArguments(parameters); + output = registeredVariant.method().invokeWithArguments(parameters); } catch (Throwable e) { Dispensers.dispense(e); return false; // never going to reach this place diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandNode.java b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandNode.java index 6268b01db..197a47c31 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandNode.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandNode.java @@ -15,7 +15,7 @@ /** * To be implemented by command classes to provide general functionality */ -public interface CommandNode { +public interface CommandNode extends GenericNode { /** * This method should be overriden by implementations in order to provide fallback implementation for the cmd system diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandOption.java b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandOption.java index 2e76e099d..531779e63 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/CommandOption.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/CommandOption.java @@ -21,7 +21,7 @@ private final CommandManager commandManager; - private final List registeredCommandVariants = new ArrayList<>(); + private final List registeredVariants = new ArrayList<>(); private final CommandInfo commandInfo = new CommandInfo(); private final Map keywordIgnoreCaseMap = new HashMap<>(); @@ -35,7 +35,7 @@ public CommandContext handle(RegisteredCommand registeredCommand, String commandName, String[] args, Sender sender, int index) { if (args.length == 0 && index == 0) { - if (!registeredCommandVariants.isEmpty() && argumentTypeOptionalHandlerMap.isEmpty()) + if (!registeredVariants.isEmpty() && argumentTypeOptionalHandlerMap.isEmpty()) return new CommandContext( sender, commandInfo, @@ -75,7 +75,7 @@ public CommandContext handle(RegisteredCommand registeredCommand, String command commandError = result; } - AbstractCommandProcessingContext context = new AbstractCommandProcessingContext(sender, commandName, args, index); + SimpleCommandProcessingContext context = new SimpleCommandProcessingContext(sender, commandName, args, index); for (Entry, CommandBranchProcessor> entry : argumentTypeHandlerMap) { ArgumentParser typeParser = entry.key().parser(); @@ -114,7 +114,7 @@ public CommandContext handle(RegisteredCommand registeredCommand, String command } result.parsedArgs() - .computeIfAbsent(typeParser.getArgumentType(), (clazz) -> new ArrayList<>()) + .computeIfAbsent(typeParser.getArgumentType(), clazz -> new ArrayList<>()) .add(0, Optional.empty()); return result; @@ -144,7 +144,7 @@ public CommandContext handle(RegisteredCommand registeredCommand, String command } result.parsedArgs() - .computeIfAbsent(typeParser.getArgumentType(), (clazz) -> new ArrayList<>()) + .computeIfAbsent(typeParser.getArgumentType(), clazz -> new ArrayList<>()) .add(0, parse.get().result()); return result; @@ -180,7 +180,7 @@ public CommandContext handle(RegisteredCommand registeredCommand, String command if (result == null) { - if (!registeredCommandVariants.isEmpty()) + if (!registeredVariants.isEmpty()) return new CommandContext( sender, commandInfo, @@ -203,7 +203,7 @@ public CommandContext handle(RegisteredCommand registeredCommand, String command result.parsedArgs() - .computeIfAbsent(entry.key().parser().getArgumentType(), (clazz) -> new ArrayList<>()) + .computeIfAbsent(entry.key().parser().getArgumentType(), clazz -> new ArrayList<>()) .add(0, Optional.empty()); return result; @@ -243,7 +243,7 @@ public Set handleTabCompletion(RegisteredCommand registeredCommand, Stri if (!entry.value().haveAnyRequirementsMeet(sender, commandName, args, index)) continue; - Optional tabCompletionResult = entry.key().tabCompletion(new AbstractCommandProcessingContext(sender, commandName, args, index)); + Optional tabCompletionResult = entry.key().tabCompletion(new SimpleCommandProcessingContext(sender, commandName, args, index)); if (tabCompletionResult.isEmpty()) { if (entry.key().isOptional()) { if (!entry.key().optionalNotMatch()) @@ -291,12 +291,12 @@ public Set handleTabCompletion(RegisteredCommand registeredCommand, Stri for (Entry, CommandBranchProcessor> entry : argumentTypeHandlerMap) { ArgumentParser typeParser = entry.key(); - OptionalInt parse = typeParser.tryParse(new AbstractCommandProcessingContext(sender, commandName, args, index)); + OptionalInt parse = typeParser.tryParse(new SimpleCommandProcessingContext(sender, commandName, args, index)); if (parse.isEmpty()) { if (!entry.value().haveAnyRequirementsMeet(sender, commandName, args, index)) continue; - Optional tabCompletionResult = entry.key().tabCompletion(new AbstractCommandProcessingContext(sender, commandName, args, index)); + Optional tabCompletionResult = entry.key().tabCompletion(new SimpleCommandProcessingContext(sender, commandName, args, index)); if (tabCompletionResult.isEmpty()) { if (entry.key().isOptional()) { if (!entry.key().optionalNotMatch()) @@ -317,7 +317,7 @@ public Set handleTabCompletion(RegisteredCommand registeredCommand, Stri if (parse.getAsInt() <= args.length) { if (entry.value().haveAnyRequirementsMeet(sender, commandName, args, index)) { - Optional tabCompletionResult = entry.key().tabCompletion(new AbstractCommandProcessingContext(sender, commandName, args, index)); + Optional tabCompletionResult = entry.key().tabCompletion(new SimpleCommandProcessingContext(sender, commandName, args, index)); if (tabCompletionResult.isPresent()) { if (tabCompletionResult.get().newIndex() >= args.length) { result.addAll(tabCompletionResult.get().result().stream().toList()); @@ -342,8 +342,8 @@ public Set handleTabCompletion(RegisteredCommand registeredCommand, Stri return result; } - public List getRegisteredCommandVariants() { - return registeredCommandVariants; + public List getRegisteredCommandVariants() { + return registeredVariants; } public CommandInfo getCommandInfo() { diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/GenericNode.java b/commands/src/main/java/net/apartium/cocoabeans/commands/GenericNode.java new file mode 100644 index 000000000..ef1b78f03 --- /dev/null +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/GenericNode.java @@ -0,0 +1,15 @@ +package net.apartium.cocoabeans.commands; + +import org.jetbrains.annotations.ApiStatus; + +/** + * A generic node interface + * Is use for objects that are related to commands or other commands related objects like compound parsers and flags + * + * @see CommandNode + * @see net.apartium.cocoabeans.commands.parsers.CompoundParser + */ +@ApiStatus.AvailableSince("0.0.37") +public interface GenericNode { + +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredCommand.java b/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredCommand.java index a1130f260..27a784d15 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredCommand.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredCommand.java @@ -24,18 +24,17 @@ import net.apartium.cocoabeans.reflect.MethodUtils; import net.apartium.cocoabeans.structs.Entry; -import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandles; import java.lang.reflect.*; import java.util.*; import java.util.stream.Stream; +import static net.apartium.cocoabeans.commands.RegisteredVariant.REGISTERED_VARIANT_COMPARATOR; + /*package-private*/ class RegisteredCommand { private static final Comparator HANDLE_EXCEPTION_VARIANT_COMPARATOR = (a, b) -> Integer.compare(b.priority(), a.priority()); - private static final Comparator REGISTERED_COMMAND_VARIANT_COMPARATOR = (a, b) -> Integer.compare(b.priority(), a.priority()); - public record RegisteredCommandNode(CommandNode listener, RequirementSet requirements) {} private final CommandManager commandManager; @@ -70,19 +69,23 @@ public void addNode(CommandNode node) { node, new RequirementSet( requirementSet, - createRequirementSet(node, fallbackHandle.getAnnotations()) + RequirementFactory.createRequirementSet(node, fallbackHandle.getAnnotations(), commandManager.requirementFactories) )) ); Map> argumentTypeHandlerMap = new HashMap<>(); MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); // Add class parsers & class parsers - serializeAllClassParser(clazz, node, argumentTypeHandlerMap); - - for (var entry : commandManager.argumentTypeHandlerMap.entrySet()) { - argumentTypeHandlerMap.putIfAbsent(entry.getKey(), entry.getValue()); - } + CollectionHelpers.mergeInto( + argumentTypeHandlerMap, + ParserFactory.findClassParsers(node, clazz, commandManager.parserFactories) + ); + // Add command manager argument parsers + CollectionHelpers.mergeInto( + argumentTypeHandlerMap, + commandManager.argumentTypeHandlerMap + ); List classRequirementsResult = new ArrayList<>(); CommandOption commandOption = createCommandOption(requirementSet, commandBranchProcessor, classRequirementsResult); @@ -151,30 +154,6 @@ private void serializeExceptionHandles(Method method, CommandNode node, MethodHa } - private void serializeAllClassParser(Class clazz, CommandNode node, Map> argumentTypeHandlerMap) { - for (Class c : ClassUtils.getSuperClassAndInterfaces(clazz)) { - for (var entry : serializeArgumentTypeHandler(node, c.getAnnotations(), c, true).entrySet()) { - argumentTypeHandlerMap.putIfAbsent(entry.getKey(), entry.getValue()); - } - - for (Method method : c.getMethods()) { - try { - findParsers( - node, - argumentTypeHandlerMap, - clazz.getMethod(method.getName(), method.getParameterTypes()), - method, - true - ); - - } catch (NoSuchMethodException ignored) { - // ignored - } - } - - } - } - private void handleSubCommand(ParserSubCommandContext context, MethodHandles.Lookup publicLookup, Method targetMethod, List classRequirementsResult) throws IllegalAccessException { ExceptionHandle exceptionHandle; if (targetMethod == null) @@ -205,16 +184,6 @@ private void handleSubCommand(ParserSubCommandContext context, MethodHandles.Loo } } - - private void findParsers(CommandNode node, Map> argumentTypeHandlerMap, Method method, Method targetMethod, boolean onlyClassParser) { - Annotation[] annotations = targetMethod.getAnnotations(); - - for (Annotation annotation : annotations) { - handleParserFactories(node, method, argumentTypeHandlerMap, annotation, onlyClassParser); - } - - } - private void parseSubCommand(ParserSubCommandContext context, MethodHandles.Lookup publicLookup, List> parsersResult, List requirementsResult) throws IllegalAccessException { if (context.subCommand == null) return; @@ -226,12 +195,12 @@ private void parseSubCommand(ParserSubCommandContext context, MethodHandles.Look throw new IllegalAccessException("Static method " + context.clazz.getName() + "#" + context.method.getName() + " is not supported"); - Map> methodArgumentTypeHandlerMap = new HashMap<>(serializeArgumentTypeHandler(context.commandNode, context.method.getAnnotations(), context.method, false)); + Map> methodArgumentTypeHandlerMap = new HashMap<>(ParserFactory.getArgumentParsers(context.commandNode, context.method.getAnnotations(), context.method, false, commandManager.parserFactories)); for (Method targetMethod : MethodUtils.getMethodsFromSuperClassAndInterface(context.method)) { CollectionHelpers.mergeInto( methodArgumentTypeHandlerMap, - serializeArgumentTypeHandler(context.commandNode, targetMethod.getAnnotations(), targetMethod, false) + ParserFactory.getArgumentParsers(context.commandNode, targetMethod.getAnnotations(), targetMethod, false, commandManager.parserFactories) ); } @@ -253,19 +222,19 @@ private void parseSubCommand(ParserSubCommandContext context, MethodHandles.Look CommandOption cmdOption = createCommandOption(methodRequirements, commandBranchProcessor, requirementsResult); cmdOption.getCommandInfo().fromCommandInfo(methodInfo); - RegisteredCommandVariant.Parameter[] parameters = serializeParameters(context.commandNode, context.method.getParameters()); + RegisteredVariant.Parameter[] parameters = RegisteredVariant.Parameter.of(context.commandNode, context.method.getParameters(), commandManager.argumentRequirementFactories); try { CollectionHelpers.addElementSorted( cmdOption.getRegisteredCommandVariants(), - new RegisteredCommandVariant( + new RegisteredVariant( publicLookup.unreflect(context.method), parameters, context.commandNode, commandManager.getArgumentMapper().mapIndices(parameters, parsersResult, requirementsResult), context.subCommand.priority() ), - REGISTERED_COMMAND_VARIANT_COMPARATOR + REGISTERED_VARIANT_COMPARATOR ); } catch (IllegalAccessException e) { throw new RuntimeException("Error accessing method", e); @@ -299,18 +268,18 @@ private void parseSubCommand(ParserSubCommandContext context, MethodHandles.Look currentCommandOption.getCommandInfo().fromCommandInfo(methodInfo); - RegisteredCommandVariant.Parameter[] parameters = serializeParameters(context.commandNode, context.method.getParameters()); + RegisteredVariant.Parameter[] parameters = RegisteredVariant.Parameter.of(context.commandNode, context.method.getParameters(), commandManager.argumentRequirementFactories); CollectionHelpers.addElementSorted( currentCommandOption.getRegisteredCommandVariants(), - new RegisteredCommandVariant( + new RegisteredVariant( publicLookup.unreflect(context.method), parameters, context.commandNode, commandManager.getArgumentMapper().mapIndices(parameters, parsersResult, requirementsResult), context.subCommand.priority() ), - REGISTERED_COMMAND_VARIANT_COMPARATOR + REGISTERED_VARIANT_COMPARATOR ); } @@ -390,58 +359,6 @@ private CommandOption createArgumentOption(CommandOption currentCommandOption, A return createCommandOption(requirements, commandBranchProcessor, requirementsResult); } - private RegisteredCommandVariant.Parameter[] serializeParameters(CommandNode commandNode, Parameter[] parameters) { - RegisteredCommandVariant.Parameter[] result = new RegisteredCommandVariant.Parameter[parameters.length]; - for (int i = 0; i < result.length; i++) { - result[i] = new RegisteredCommandVariant.Parameter( - parameters[i].getType(), - parameters[i].getParameterizedType(), - serializeArgumentRequirement(commandNode, parameters[i].getAnnotations()), - serializeParameterName(parameters[i]) - ); - } - return result; - } - - private String serializeParameterName(Parameter parameter) { - String name = Optional.ofNullable(parameter.getAnnotation(Param.class)) - .map(Param::value) - .orElse(null); - - if (name == null) - return null; - - if (name.isEmpty()) - throw new IllegalArgumentException("Parameter name cannot be empty"); - - return name; - } - - private ArgumentRequirement[] serializeArgumentRequirement(CommandNode commandNode, Annotation[] annotations) { - List result = new ArrayList<>(); - - for (Annotation annotation : annotations) { - Class argumentRequirementType = ArgumentRequirementFactory.getArgumentRequirementFactoryClass(annotation); - - if (argumentRequirementType == null) - continue; - - ArgumentRequirement argumentRequirement = Optional.ofNullable(commandManager.argumentRequirementFactories.computeIfAbsent( - argumentRequirementType, - clazz -> ArgumentRequirementFactory.createFromAnnotation(annotation, commandManager) - )) - .map(factory -> factory.getArgumentRequirement(commandNode, annotation)) - .orElse(null); - - if (argumentRequirement == null) - continue; - - result.add(argumentRequirement); - } - - return result.toArray(new ArgumentRequirement[0]); - } - private CommandOption createCommandOption(RequirementSet requirements, CommandBranchProcessor branchProcessor, List requirementsResult) { CommandOption cmdOption = branchProcessor.objectMap.stream() .filter(entry -> entry.key().equals(requirements)) @@ -466,97 +383,21 @@ private Set findAllRequirements(CommandNode commandNode, Class c Set requirements = new HashSet<>(); for (Class c : ClassUtils.getSuperClassAndInterfaces(clazz)) { - requirements.addAll(createRequirementSet(commandNode, c.getAnnotations())); + requirements.addAll(RequirementFactory.createRequirementSet(commandNode, c.getAnnotations(), commandManager.requirementFactories)); } return requirements; } private Set findAllRequirements(CommandNode commandNode, Method method) { - Set requirements = new HashSet<>(createRequirementSet(commandNode, method.getAnnotations())); + Set requirements = new HashSet<>(RequirementFactory.createRequirementSet(commandNode, method.getAnnotations(), commandManager.requirementFactories)); for (Method target : MethodUtils.getMethodsFromSuperClassAndInterface(method)) { - requirements.addAll(createRequirementSet(commandNode, target.getAnnotations())); + requirements.addAll(RequirementFactory.createRequirementSet(commandNode, target.getAnnotations(), commandManager.requirementFactories)); } return requirements; } - private Requirement getRequirement(CommandNode commandNode, Annotation annotation) { - Class requirementFactoryClass = RequirementFactory.getRequirementFactoryClass(annotation); - - if (requirementFactoryClass == null) - return null; - - RequirementFactory requirementFactory = commandManager.requirementFactories.computeIfAbsent( - requirementFactoryClass, - clazz -> RequirementFactory.createFromAnnotation(annotation) - ); - - if (requirementFactory == null) - return null; - - return requirementFactory.getRequirement(commandNode, annotation); - } - - - private Set createRequirementSet(CommandNode commandNode, Annotation[] annotations) { - if (annotations == null || annotations.length == 0) - return Collections.emptySet(); - - Set requirements = new HashSet<>(); - - for (Annotation annotation : annotations) { - Requirement requirement = getRequirement(commandNode, annotation); - if (requirement == null) - continue; - - requirements.add(requirement); - } - - return requirements; - } - - private Map> serializeArgumentTypeHandler(CommandNode commandNode, Annotation[] annotations, GenericDeclaration obj, boolean onlyClassParser) { - Map> argumentTypeHandlerMap = new HashMap<>(); - - - if (annotations == null) { - return argumentTypeHandlerMap; - } - - for (Annotation annotation : annotations) { - handleParserFactories(commandNode, obj, argumentTypeHandlerMap, annotation, onlyClassParser); - } - - return argumentTypeHandlerMap; - } - - private void handleParserFactories(CommandNode commandNode, GenericDeclaration obj, Map> argumentTypeHandlerMap, Annotation annotation, boolean onlyClassParser) { - Class parserFactoryClass = ParserFactory.getParserFactoryClass(annotation); - - if (parserFactoryClass == null) - return; - - ParserFactory parserFactory = commandManager.parserFactories.computeIfAbsent( - parserFactoryClass, - clazz -> ParserFactory.createFromAnnotation(annotation, onlyClassParser) - ); - - if (parserFactory == null) - return; - - Collection parserResults = parserFactory.getArgumentParser(commandNode, annotation, obj); - if (parserResults.isEmpty()) - return; - - for (ParserFactory.ParserResult parseResult : parserResults) { - if (!parseResult.scope().isClass() && onlyClassParser) - continue; - - argumentTypeHandlerMap.put(parseResult.parser().getKeyword(), parseResult.parser()); - } - } - record ParserSubCommandContext( Method method, SubCommand subCommand, diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredCommandVariant.java b/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredCommandVariant.java deleted file mode 100644 index 6a61bae77..000000000 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredCommandVariant.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2024 Apartium - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package net.apartium.cocoabeans.commands; - -import net.apartium.cocoabeans.commands.requirements.ArgumentRequirement; - -import java.lang.invoke.MethodHandle; -import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -public record RegisteredCommandVariant( - MethodHandle method, - Parameter[] parameters, - CommandNode commandNode, - List> argumentIndexList, - int priority -) { - - public record Parameter( - Class type, - Type parameterizedType, - ArgumentRequirement[] argumentRequirements, - String parameterName - ) { - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Parameter parameter)) return false; - return Objects.equals(type, parameter.type) - && Objects.equals(parameterName, parameter.parameterName) - && Objects.equals(parameterizedType, parameter.parameterizedType) - && Objects.deepEquals(argumentRequirements, parameter.argumentRequirements); - } - - @Override - public int hashCode() { - return Objects.hash( - type, - parameterizedType, - Arrays.hashCode(argumentRequirements), - parameterName - ); - } - - @Override - public String toString() { - return "Parameter{" + - "type=" + type + - ", parameterizedType=" + parameterizedType + - ", argumentRequirements=" + Arrays.toString(argumentRequirements) + - ", parameterName='" + parameterName + '\'' + - '}'; - } - } - -} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredVariant.java b/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredVariant.java new file mode 100644 index 000000000..62dc195a8 --- /dev/null +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/RegisteredVariant.java @@ -0,0 +1,150 @@ +/* + * Copyright 2024 Apartium + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.apartium.cocoabeans.commands; + +import net.apartium.cocoabeans.commands.requirements.ArgumentRequirement; +import net.apartium.cocoabeans.commands.requirements.ArgumentRequirementFactory; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.*; + +/** + * Registered variant for a command + * @param method target method to run + * @param parameters parameters + * @param node node instance + * @param argumentIndexList list of argument indexes that will be passed to the method + * @param priority the priority of the variant + */ +public record RegisteredVariant( + MethodHandle method, + Parameter[] parameters, + GenericNode node, + List> argumentIndexList, + int priority +) { + + /** + * Comparator for registered variants + */ + public static final Comparator REGISTERED_VARIANT_COMPARATOR = (a, b) -> Integer.compare(b.priority(), a.priority()); + + /** + * Parameter represent a parameter of the command after has been parsed + * @param type The type of parameter + * @param parameterizedType parameterized type + * @param argumentRequirements argument requirements + * @param parameterName parameter name + */ + public record Parameter( + Class type, + Type parameterizedType, + ArgumentRequirement[] argumentRequirements, + String parameterName + ) { + + /** + * create parameters from the given parameters + * @param node instance of the node + * @param parameters parameters + * @param argumentRequirementFactories argument requirement factories for caching + * @return parameters parsed + */ + @ApiStatus.AvailableSince("0.0.37") + public static Parameter[] of(GenericNode node, java.lang.reflect.Parameter[] parameters, Map, ArgumentRequirementFactory> argumentRequirementFactories) { + Parameter[] result = new Parameter[parameters.length]; + + for (int i = 0; i < result.length; i++) { + result[i] = new Parameter( + parameters[i].getType(), + parameters[i].getParameterizedType(), + ArgumentRequirementFactory.createArgumentRequirements(node, parameters[i].getAnnotations(), argumentRequirementFactories), + getParamName(parameters[i]) + ); + } + + return result; + } + + @ApiStatus.Internal + private static String getParamName(java.lang.reflect.Parameter parameter) { + String name = Optional.ofNullable(parameter.getAnnotation(Param.class)) + .map(Param::value) + .orElse(null); + + if (name == null) + return null; + + if (name.isEmpty()) + throw new IllegalArgumentException("Parameter name cannot be empty"); + + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Parameter parameter)) return false; + return Objects.equals(type, parameter.type) + && Objects.equals(parameterName, parameter.parameterName) + && Objects.equals(parameterizedType, parameter.parameterizedType) + && Objects.deepEquals(argumentRequirements, parameter.argumentRequirements); + } + + @Override + public int hashCode() { + return Objects.hash( + type, + parameterizedType, + Arrays.hashCode(argumentRequirements), + parameterName + ); + } + + @Override + public String toString() { + return "Parameter{" + + "type=" + type + + ", parameterizedType=" + parameterizedType + + ", argumentRequirements=" + Arrays.toString(argumentRequirements) + + ", parameterName='" + parameterName + '\'' + + '}'; + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RegisteredVariant that)) return false; + return priority == that.priority + && Objects.equals(node, that.node) + && Objects.equals(method, that.method) + && Objects.deepEquals(parameters, that.parameters) + && Objects.equals(argumentIndexList, that.argumentIndexList); + } + + @Override + public int hashCode() { + return Objects.hash(method, Arrays.hashCode(parameters), node, argumentIndexList, priority); + } + + @Override + public String toString() { + return "RegisteredVariant{" + "method=" + method + + ", parameters=" + Arrays.toString(parameters) + + ", node=" + node + + ", argumentIndexList=" + argumentIndexList + + ", priority=" + priority + + '}'; + } +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/SimpleArgumentMapper.java b/commands/src/main/java/net/apartium/cocoabeans/commands/SimpleArgumentMapper.java index 930827f42..1e6675e61 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/SimpleArgumentMapper.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/SimpleArgumentMapper.java @@ -92,7 +92,7 @@ public class SimpleArgumentMapper implements ArgumentMapper { ); @Override - public List> mapIndices(RegisteredCommandVariant.Parameter[] parameters, List> argumentParsers, List requirements) { + public List> mapIndices(RegisteredVariant.Parameter[] parameters, List> argumentParsers, List requirements) { if (parameters.length == 0) return List.of(); @@ -101,7 +101,7 @@ public List> mapIndices(RegisteredCommandVariant.Parameter[] pa Map, Integer> counterMap = new HashMap<>(); ResultMap resultMap = createParsedArgs(argumentParsers, requirements, getParametersNames(parameters)); - for (RegisteredCommandVariant.Parameter parameter : parameters) { + for (RegisteredVariant.Parameter parameter : parameters) { Class type = parameter.type(); boolean optional = false; @@ -131,10 +131,10 @@ public List> mapIndices(RegisteredCommandVariant.Parameter[] pa return result; } - private Map> getParametersNames(RegisteredCommandVariant.Parameter[] parameters) { + private Map> getParametersNames(RegisteredVariant.Parameter[] parameters) { Map> result = new HashMap<>(); - for (RegisteredCommandVariant.Parameter parameter : parameters) { + for (RegisteredVariant.Parameter parameter : parameters) { if (parameter.parameterName() == null) continue; diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/AbstractCommandProcessingContext.java b/commands/src/main/java/net/apartium/cocoabeans/commands/SimpleCommandProcessingContext.java similarity index 68% rename from commands/src/main/java/net/apartium/cocoabeans/commands/AbstractCommandProcessingContext.java rename to commands/src/main/java/net/apartium/cocoabeans/commands/SimpleCommandProcessingContext.java index 72011a09b..7a38c9bc4 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/AbstractCommandProcessingContext.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/SimpleCommandProcessingContext.java @@ -17,12 +17,13 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import java.util.HashMap; import java.util.List; -import java.util.Map; -@ApiStatus.Internal -/* package-private */ class AbstractCommandProcessingContext implements CommandProcessingContext { +/** + * A simple implementation of {@link CommandProcessingContext} + */ +@ApiStatus.AvailableSince("0.0.37") +public class SimpleCommandProcessingContext implements CommandProcessingContext { @NotNull private final Sender sender; @@ -35,47 +36,87 @@ private BadCommandResponse error = null; - /* package-private */ AbstractCommandProcessingContext(@NotNull Sender sender, String label, String[] args, int index) { + /** + * Create a simple command processing context + * @param sender sender + * @param label label (command name) + * @param args args split by spaces + * @param index current index + */ + public SimpleCommandProcessingContext(@NotNull Sender sender, String label, String[] args, int index) { this.sender = sender; this.args = List.of(args); this.index = index; this.label = label; } + /** + * Returns the sender + * @return sender + */ @Override public @NotNull Sender sender() { return this.sender; } + /** + * Returns the command name + * @return command name + */ @Override public String label() { return label; } + /** + * Returns the arguments + * @return arguments + */ @Override public List args() { return this.args; } + /** + * Returns the current index + * @return current index + */ @Override public int index() { return this.index; } + /** + * check if sender meets requirement + * @param requirement requirement to meet + * @return requirement result + */ @Override public RequirementResult senderMeetsRequirement(Requirement requirement) { return requirement.meetsRequirement(new RequirementEvaluationContext(sender, label, args.toArray(new String[0]), index)); } + /** + * report a problem with command parsing + * @param source source reporter of the problem + * @param response response error description object + */ @Override public void report(Object source, @NotNull BadCommandResponse response) { error = response; } + /** + * Retrieving the current report + * @return current report + */ public BadCommandResponse getReport() { return error; } + /** + * Clear the current report + */ public void clearReports() { error = null; } diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParser.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParser.java new file mode 100644 index 000000000..cb6bf60a6 --- /dev/null +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParser.java @@ -0,0 +1,269 @@ +package net.apartium.cocoabeans.commands.parsers; + +import net.apartium.cocoabeans.CollectionHelpers; +import net.apartium.cocoabeans.Dispensers; +import net.apartium.cocoabeans.commands.*; +import net.apartium.cocoabeans.commands.exception.BadCommandResponse; +import net.apartium.cocoabeans.commands.exception.UnknownTokenException; +import net.apartium.cocoabeans.commands.lexer.ArgumentParserToken; +import net.apartium.cocoabeans.commands.lexer.CommandLexer; +import net.apartium.cocoabeans.commands.lexer.CommandToken; +import net.apartium.cocoabeans.commands.lexer.KeywordToken; +import net.apartium.cocoabeans.commands.requirements.*; +import net.apartium.cocoabeans.structs.Entry; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.*; + +import static net.apartium.cocoabeans.commands.RegisteredVariant.REGISTERED_VARIANT_COMPARATOR; + +@ApiStatus.AvailableSince("0.0.37") +public class CompoundParser extends ArgumentParser implements GenericNode { + + private final Map, RequirementFactory> requirementFactories = new HashMap<>(); + private final Map, ParserFactory> parserFactories = new HashMap<>(); + private final Map, ArgumentRequirementFactory> argumentRequirementFactories = new HashMap<>(); + + private final CompoundParserBranchProcessor compoundParserBranchProcessor; + + private final ArgumentMapper argumentMapper; + private final CommandLexer commandLexer; + + + /** + * Constructs a new parser + * + * @param keyword keyword of the parser + * @param clazz output class + * @param priority priority + */ + protected CompoundParser(String keyword, Class clazz, int priority, ArgumentMapper argumentMapper, CommandLexer commandLexer) { + super(keyword, clazz, priority); + + this.argumentMapper = argumentMapper; + + this.commandLexer = commandLexer; + this.compoundParserBranchProcessor = new CompoundParserBranchProcessor<>(); + + try { + createBranch(); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to create branch", e); + } + + // clear cache + this.requirementFactories.clear(); + this.parserFactories.clear(); + this.argumentRequirementFactories.clear(); + } + + private void createBranch() throws IllegalAccessException { + RequirementSet requirementsResult = RequirementFactory.createRequirementSet(this, this.getClass().getAnnotations(), requirementFactories); + Map> argumentParser = new HashMap<>(); + + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + + CollectionHelpers.mergeInto( + argumentParser, + ParserFactory.findClassParsers(this, this.getClass(), parserFactories) + ); + + for (Method method : this.getClass().getMethods()) { + ParserVariant[] parserVariants = method.getAnnotationsByType(ParserVariant.class); + if (parserVariants.length == 0) + continue; + + RequirementSet methodRequirements = new RequirementSet( + RequirementFactory.createRequirementSet(this, method.getAnnotations(), requirementFactories), + requirementsResult + ); + + Map> methodArgumentTypeHandlerMap = new HashMap<>(argumentParser); + methodArgumentTypeHandlerMap.putAll(ParserFactory.getArgumentParsers(this, method.getAnnotations(), method, false, parserFactories)); + + RegisteredVariant.Parameter[] parameters = RegisteredVariant.Parameter.of(this, method.getParameters(), argumentRequirementFactories); + + for (ParserVariant parserVariant : parserVariants) { + handleParserVariants(lookup.unreflect(method), parserVariant, methodRequirements, methodArgumentTypeHandlerMap, parameters, new ArrayList<>(), new ArrayList<>(methodRequirements)); + } + } + } + + private void handleParserVariants(MethodHandle method, ParserVariant parserVariant, RequirementSet requirementSet, Map> argumentParserMap, RegisteredVariant.Parameter[] parameters, List> parsersResult, List requirementsResult) { + List tokens = commandLexer.tokenize(parserVariant.value()); + + if (tokens.isEmpty()) + throw new IllegalArgumentException("Parser variant cannot be empty"); + + CompoundParserOption currentOption = findOrCreateOption(this.compoundParserBranchProcessor, requirementSet, new ArrayList<>()); + + + for (int i = 0; i < tokens.size(); i++) { + CommandToken token = tokens.get(i); + + RequirementSet requirements = i == 0 ? requirementSet : new RequirementSet(); + + + if (token instanceof KeywordToken) + throw new UnsupportedOperationException("Keyword tokens are not supported"); + + + if (token instanceof ArgumentParserToken argumentParserToken) { + currentOption = createArgumentOption(currentOption, argumentParserToken, argumentParserMap, requirements, parsersResult, requirementsResult); + continue; + } + + throw new UnknownTokenException(token); + } + + + CollectionHelpers.addElementSorted( + currentOption.getRegisteredVariants(), + new RegisteredVariant( + method, + parameters, + this, + argumentMapper.mapIndices(parameters, parsersResult, requirementsResult), + parserVariant.priority() + ), + REGISTERED_VARIANT_COMPARATOR + ); + } + + private CompoundParserOption createArgumentOption(CompoundParserOption option, ArgumentParserToken argumentParserToken, Map> argumentTypeHandlerMap, RequirementSet requirements, List> parsersResult, List requirementsResult) { + RegisterArgumentParser parser = argumentParserToken.getParser(argumentTypeHandlerMap); + if (parser == null) + throw new IllegalArgumentException("Parser not found: " + argumentParserToken.getParserName()); + + Entry, CompoundParserBranchProcessor> entryArgument = option.argumentTypeHandlerMap.stream() + .filter(entry -> entry.key().equals(parser)) + .findAny() + .orElse(null); + + CompoundParserBranchProcessor branchProcessor = entryArgument == null + ? null + : entryArgument.value(); + + parsersResult.add(entryArgument == null ? parser : entryArgument.key()); + + if (branchProcessor == null) { + branchProcessor = new CompoundParserBranchProcessor<>(); + + CollectionHelpers.addElementSorted( + option.argumentTypeHandlerMap, + new Entry<>( + parser, + branchProcessor + ), + (a, b) -> b.key().compareTo(a.key()) + ); + + } + + return findOrCreateOption(branchProcessor, requirements, requirementsResult); + } + + @ApiStatus.Internal + private CompoundParserOption findOrCreateOption(CompoundParserBranchProcessor branchProcessor, RequirementSet requirements, List requirementsResult) { + for (Entry> entry : branchProcessor.objectMap) { + if (entry.key().equals(requirements)) + return entry.value(); + } + + CompoundParserOption option = new CompoundParserOption<>(); + branchProcessor.objectMap.add(new Entry<>( + requirements, + option + )); + + requirementsResult.addAll(requirements); + + return option; + } + + @ApiStatus.Internal + private List getParameters(RegisteredVariant registeredVariant, Sender sender, ArgumentContext context) { + List parameters = new ArrayList<>(registeredVariant.argumentIndexList().stream() + .map((argumentIndex -> argumentIndex.get(context))) + .toList()); + + parameters.add(0, registeredVariant.node()); + + for (int i = 0; i < registeredVariant.parameters().length; i++) { + Object obj = parameters.get(i + 1); // first element is class instance + for (ArgumentRequirement argumentRequirement : registeredVariant.parameters()[i].argumentRequirements()) { + if (!argumentRequirement.meetsRequirement(sender, null, obj)) + return null; + } + } + + return parameters; + } + + + @Override + public Optional> parse(CommandProcessingContext processingContext) { + Optional parse = compoundParserBranchProcessor.parse(processingContext); + + if (parse.isEmpty()) { + processingContext.report(this, new BadCommandResponse(processingContext.label(), processingContext.args().toArray(new String[0]), processingContext.index(), "No variant found")); + return Optional.empty(); + } + + for (RegisteredVariant registeredVariant : parse.get().registeredVariant()) { + List parameters = getParameters( + registeredVariant, + processingContext.sender(), + new ArgumentContext(processingContext.label(), processingContext.args().toArray(new String[0]), processingContext.sender(), parse.get().mappedByClass) + ); + + if (parameters == null) + continue; + + T output; + try { + output = (T) registeredVariant.method().invokeWithArguments(parameters); + } catch (Throwable e) { + Dispensers.dispense(e); + return Optional.empty(); // never going to reach this place + } + + if (output == null) + continue; + + return Optional.of(new ParseResult<>( + output, + parse.get().newIndex + )); + + } + processingContext.report(this, new BadCommandResponse(processingContext.label(), processingContext.args().toArray(new String[0]), processingContext.index(), "No variant found")); + return Optional.empty(); + } + + @Override + public OptionalInt tryParse(CommandProcessingContext processingContext) { + return parse(processingContext) + .map(ParseResult::newIndex) + .map(OptionalInt::of) + .orElse(OptionalInt.empty()); + } + + @Override + public Optional tabCompletion(CommandProcessingContext processingContext) { + return compoundParserBranchProcessor.tabCompletion(processingContext); + } + + + /* package-private */ record ParserResult ( + List registeredVariant, + int newIndex, + Map, List> mappedByClass + ) { + + } + +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParserBranchProcessor.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParserBranchProcessor.java new file mode 100644 index 000000000..3b3902e23 --- /dev/null +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParserBranchProcessor.java @@ -0,0 +1,65 @@ +package net.apartium.cocoabeans.commands.parsers; + +import net.apartium.cocoabeans.commands.CommandProcessingContext; +import net.apartium.cocoabeans.commands.requirements.RequirementEvaluationContext; +import net.apartium.cocoabeans.commands.requirements.RequirementResult; +import net.apartium.cocoabeans.commands.requirements.RequirementSet; +import net.apartium.cocoabeans.structs.Entry; + +import java.util.*; + +/* package-private */ class CompoundParserBranchProcessor { + + final List>> objectMap = new ArrayList<>(); + + public Optional parse(CommandProcessingContext processingContext) { + RequirementEvaluationContext requirementEvaluationContext = new RequirementEvaluationContext(processingContext.sender(), processingContext.label(), processingContext.args().toArray(new String[0]), processingContext.index()); + + for (Entry> entry : objectMap) { + RequirementResult requirementResult = entry.key().meetsRequirements(requirementEvaluationContext); + + if (!requirementResult.meetRequirement()) + continue; + + Optional result = entry.value().parse(processingContext); + if (result.isEmpty()) + continue; + + for (RequirementResult.Value value : requirementResult.getValues()) { + result.get().mappedByClass() + .computeIfAbsent(value.clazz(), clazz -> new ArrayList<>()) + .add(value.value()); + } + + return result; + } + + return Optional.empty(); + } + + public Optional tabCompletion(CommandProcessingContext processingContext) { + RequirementEvaluationContext requirementEvaluationContext = new RequirementEvaluationContext(processingContext.sender(), processingContext.label(), processingContext.args().toArray(new String[0]), processingContext.index()); + + Set result = new HashSet<>(); + int highestIndex = -1; + + for (Entry> entry : objectMap) { + if (!entry.key().meetsRequirements(requirementEvaluationContext).meetRequirement()) + continue; + + Optional tabCompletion = entry.value().tabCompletion(processingContext); + if (tabCompletion.isEmpty()) + continue; + + highestIndex = Math.max(highestIndex, tabCompletion.get().newIndex()); + result.addAll(tabCompletion.get().result()); + } + + if (result.isEmpty()) + return Optional.empty(); + + return Optional.of(new ArgumentParser.TabCompletionResult(result, highestIndex)); + } + + +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParserOption.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParserOption.java new file mode 100644 index 000000000..406116ac4 --- /dev/null +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/CompoundParserOption.java @@ -0,0 +1,80 @@ +package net.apartium.cocoabeans.commands.parsers; + +import net.apartium.cocoabeans.commands.SimpleCommandProcessingContext; +import net.apartium.cocoabeans.commands.CommandProcessingContext; +import net.apartium.cocoabeans.commands.RegisterArgumentParser; +import net.apartium.cocoabeans.commands.RegisteredVariant; +import net.apartium.cocoabeans.structs.Entry; + +import java.util.*; + +/* package-private */ class CompoundParserOption { + + private final List registeredVariants = new ArrayList<>(); + + final List, CompoundParserBranchProcessor>> argumentTypeHandlerMap = new ArrayList<>(); + + + public Optional parse(CommandProcessingContext processingContext) { + if (processingContext.args().size() <= processingContext.index() || argumentTypeHandlerMap.isEmpty()) { + if (!registeredVariants.isEmpty()) + return Optional.of(new CompoundParser.ParserResult(Collections.unmodifiableList(registeredVariants), processingContext.index(), new HashMap<>())); + + return Optional.empty(); + } + + for (Entry, CompoundParserBranchProcessor> entry : argumentTypeHandlerMap) { + Optional> parse = entry.key().parse(processingContext); + + if (parse.isEmpty()) + continue; + + Optional result = entry.value().parse(new SimpleCommandProcessingContext( + processingContext.sender(), + processingContext.label(), + processingContext.args().toArray(new String[0]), + parse.get().newIndex() + )); + + if (result.isEmpty()) + continue; + + result.get().mappedByClass() + .computeIfAbsent(entry.key().getArgumentType(), clazz -> new ArrayList<>()) + .add(0, parse.get().result()); + + return result; + } + + if (!registeredVariants.isEmpty()) + return Optional.of(new CompoundParser.ParserResult(Collections.unmodifiableList(registeredVariants), processingContext.index(), new HashMap<>())); + + return Optional.empty(); + } + + + public Optional tabCompletion(CommandProcessingContext processingContext) { + Set result = new HashSet<>(); + + int highestIndex = -1; + + for (Entry, CompoundParserBranchProcessor> entry : argumentTypeHandlerMap) { + Optional parse = entry.key().tabCompletion(processingContext); + + if (parse.isEmpty()) + continue; + + highestIndex = Math.max(highestIndex, parse.get().newIndex()); + result.addAll(parse.get().result()); + } + + if (result.isEmpty()) + return Optional.empty(); + + return Optional.of(new ArgumentParser.TabCompletionResult(result, highestIndex)); + } + + public List getRegisteredVariants() { return registeredVariants; } + + +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserFactory.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserFactory.java index 5461392a5..c74132115 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserFactory.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserFactory.java @@ -1,13 +1,18 @@ package net.apartium.cocoabeans.commands.parsers; -import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.CollectionHelpers; +import net.apartium.cocoabeans.commands.GenericNode; +import net.apartium.cocoabeans.reflect.ClassUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; import java.lang.reflect.GenericDeclaration; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; /** * A factory associated with specific annotations to create argument parsers based on annotations present in the command class @@ -18,6 +23,95 @@ @ApiStatus.AvailableSince("0.0.30") public interface ParserFactory { + /** + * Get argument parsers from a class + * @param node command node + * @param clazz target class + * @param parserFactories parser factories for caching + * @return argument parsers + */ + @ApiStatus.AvailableSince("0.0.37") + static Map> findClassParsers(GenericNode node, Class clazz, Map, ParserFactory> parserFactories) { + Map> result = new HashMap<>(); + + for (Class c : ClassUtils.getSuperClassAndInterfaces(clazz)) { + CollectionHelpers.mergeInto( + result, + getArgumentParsers(node, c.getAnnotations(), c, true, parserFactories) + ); + + for (Method method : c.getMethods()) { + try { + result.putAll(ParserFactory.getArgumentParsers(node, method.getAnnotations(), clazz.getMethod(method.getName(), method.getParameterTypes()), true, parserFactories)); + } catch (NoSuchMethodException ignored) { + // ignored + } + } + } + + return result; + } + + /** + * Get argument parsers from an array of annotations + * @param node command node + * @param annotations annotations + * @param obj the class / method + * @param limitToClassParsers limit to class parsers only + * @param parserFactories parser factories for caching + * @return argument parsers + */ + @ApiStatus.AvailableSince("0.0.37") + static Map> getArgumentParsers(GenericNode node, Annotation[] annotations, GenericDeclaration obj, boolean limitToClassParsers, Map, ParserFactory> parserFactories) { + Map> result = new HashMap<>(); + + for (Annotation annotation : annotations) + result.putAll(getArgumentParsers(node, annotation, obj, limitToClassParsers, parserFactories)); + + return result; + } + + /** + * Get argument parsers from an annotation + * @param node command node + * @param annotation annotation + * @param obj the class / method + * @param limitToClassParsers limit to class parsers only + * @param parserFactories parser factories for caching + * @return argument parsers + */ + @ApiStatus.AvailableSince("0.0.37") + static Map> getArgumentParsers(GenericNode node, Annotation annotation, GenericDeclaration obj, boolean limitToClassParsers, Map, ParserFactory> parserFactories) { + Class parserFactoryClass = ParserFactory.getParserFactoryClass(annotation); + + if (parserFactoryClass == null) + return Map.of(); + + ParserFactory parserFactory = parserFactories.computeIfAbsent( + parserFactoryClass, + clazz -> ParserFactory.createFromAnnotation(annotation, limitToClassParsers) + ); + + if (parserFactory == null) + return Map.of(); + + Collection parserResults = parserFactory.getArgumentParser(node, annotation, obj); + + if (parserResults.isEmpty()) + return Map.of(); + + Map> result = new HashMap<>(); + + for (ParserFactory.ParserResult parseResult : parserResults) { + if (!parseResult.scope().isClass() && limitToClassParsers) + continue; + + result.put(parseResult.parser().getKeyword(), parseResult.parser()); + } + + return result; + } + /** * Get the class of the parser factory from an annotation * @param annotation The annotation that has a CommandParserFactory @@ -56,13 +150,13 @@ static ParserFactory createFromAnnotation(Annotation annotation, boolean onlyCla /** * Construct argument parsers for the given annotation - * @param commandNode The command node that the annotation is present on + * @param node The node of the command / compound parser * @param annotation The annotation to construct the argument parser for * @param obj The annotated element, either a method or a class * @return A collection of argument parsers to be registered to the scope */ @NotNull - Collection getArgumentParser(CommandNode commandNode, Annotation annotation, GenericDeclaration obj); + Collection getArgumentParser(GenericNode node, Annotation annotation, GenericDeclaration obj); /** * Represents a parser to be registered diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserVariant.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserVariant.java new file mode 100644 index 000000000..134d3bc7f --- /dev/null +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserVariant.java @@ -0,0 +1,19 @@ +package net.apartium.cocoabeans.commands.parsers; + +import org.jetbrains.annotations.ApiStatus; + +import java.lang.annotation.*; + +/** + * Parser variant is a variant of compound parser + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(ParserVariants.class) +@ApiStatus.AvailableSince("0.0.37") +public @interface ParserVariant { + + String value(); + int priority() default 0; + +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserVariants.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserVariants.java new file mode 100644 index 000000000..79d917551 --- /dev/null +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/ParserVariants.java @@ -0,0 +1,11 @@ +package net.apartium.cocoabeans.commands.parsers; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ParserVariants { + + ParserVariant[] value(); + +} diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/SourceParserFactory.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/SourceParserFactory.java index 2f95c55f4..596e5bc43 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/SourceParserFactory.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/SourceParserFactory.java @@ -2,6 +2,7 @@ import net.apartium.cocoabeans.Dispensers; import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,7 +25,7 @@ public class SourceParserFactory implements ParserFactory { @Override - public @NotNull Collection getArgumentParser(CommandNode commandNode, Annotation annotation, GenericDeclaration obj) { + public @NotNull Collection getArgumentParser(GenericNode node, Annotation annotation, GenericDeclaration obj) { if (!(annotation instanceof SourceParser sourceParser)) return Collections.emptyList(); @@ -36,7 +37,7 @@ public class SourceParserFactory implements ParserFactory { try { return List.of(new ParserResult(new SourceParserImpl<>( - commandNode, + node, sourceParser.keyword(), sourceParser.clazz(), sourceParser.priority(), @@ -53,7 +54,7 @@ public class SourceParserFactory implements ParserFactory { private static class SourceParserImpl extends MapBasedParser { - private final CommandNode node; + private final GenericNode node; private final MethodHandle handle; private final long resultMaxAgeInMills; @@ -61,7 +62,7 @@ private static class SourceParserImpl extends MapBasedParser { private Instant nextCall = Instant.now(); private Map result = null; - public SourceParserImpl(CommandNode node, String keyword, Class clazz, int priority, MethodHandle handle, long resultMaxAgeInMills, boolean ignoreCase, boolean lax) { + public SourceParserImpl(GenericNode node, String keyword, Class clazz, int priority, MethodHandle handle, long resultMaxAgeInMills, boolean ignoreCase, boolean lax) { super(keyword, clazz, priority, ignoreCase, lax); this.node = node; diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/WithParserFactory.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/WithParserFactory.java index f4ce01332..6f0d2fa01 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/WithParserFactory.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/WithParserFactory.java @@ -1,6 +1,7 @@ package net.apartium.cocoabeans.commands.parsers; import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.reflect.ConstructorUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -20,41 +21,6 @@ @ApiStatus.Internal public class WithParserFactory implements ParserFactory { - @Override - public @NotNull List getArgumentParser(CommandNode commandNode, Annotation annotation, GenericDeclaration obj) { - if (annotation instanceof WithParsers withParsers) { - List result = new ArrayList<>(withParsers.value().length); - - for (WithParser withParser : withParsers.value()) - result.addAll(getArgumentParser(commandNode, withParser, obj)); - - return result; - } - - if (!(annotation instanceof WithParser withParser)) - return List.of(); - - - try { - ArgumentParser argumentParser = newInstance( - ConstructorUtils.getDeclaredConstructors(withParser.value()), - withParser.priority(), - withParser.keyword() - ); - - if (argumentParser == null) - return List.of(); - - return List.of(new ParserResult( - argumentParser, - obj instanceof Method ? Scope.VARIANT : Scope.CLASS - )); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - SharedSecrets.LOGGER.log(System.Logger.Level.WARNING, "Failed to create WithParser", e); - return List.of(); - } - } - private static > T newInstance(Collection> constructors, int priority, String keyword) throws InstantiationException, IllegalAccessException, InvocationTargetException { Constructor matchingConstructor = null; @@ -116,4 +82,39 @@ private static > T newInstance(Collection getArgumentParser(GenericNode node, Annotation annotation, GenericDeclaration obj) { + if (annotation instanceof WithParsers withParsers) { + List result = new ArrayList<>(withParsers.value().length); + + for (WithParser withParser : withParsers.value()) + result.addAll(getArgumentParser(node, withParser, obj)); + + return result; + } + + if (!(annotation instanceof WithParser withParser)) + return List.of(); + + + try { + ArgumentParser argumentParser = newInstance( + ConstructorUtils.getDeclaredConstructors(withParser.value()), + withParser.priority(), + withParser.keyword() + ); + + if (argumentParser == null) + return List.of(); + + return List.of(new ParserResult( + argumentParser, + obj instanceof Method ? Scope.VARIANT : Scope.CLASS + )); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + SharedSecrets.LOGGER.log(System.Logger.Level.WARNING, "Failed to create WithParser", e); + return List.of(); + } + } + } diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/factory/IntRangeParserFactory.java b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/factory/IntRangeParserFactory.java index 8f2d62e2c..5210b3c99 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/factory/IntRangeParserFactory.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/parsers/factory/IntRangeParserFactory.java @@ -13,6 +13,7 @@ import net.apartium.cocoabeans.StringHelpers; import net.apartium.cocoabeans.commands.CommandNode; import net.apartium.cocoabeans.commands.CommandProcessingContext; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.commands.parsers.ArgumentParser; import net.apartium.cocoabeans.commands.parsers.IntRangeParser; import net.apartium.cocoabeans.commands.parsers.ParserFactory; @@ -33,7 +34,7 @@ public class IntRangeParserFactory implements ParserFactory { @Override - public @NotNull Collection getArgumentParser(CommandNode commandNode, Annotation annoation, GenericDeclaration obj) { + public @NotNull Collection getArgumentParser(GenericNode node, Annotation annoation, GenericDeclaration obj) { if (!(annoation instanceof IntRangeParser intRangeParser)) return Collections.emptyList(); diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/ArgumentRequirementFactory.java b/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/ArgumentRequirementFactory.java index bd592423f..a76905cb4 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/ArgumentRequirementFactory.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/ArgumentRequirementFactory.java @@ -10,17 +10,57 @@ package net.apartium.cocoabeans.commands.requirements; -import net.apartium.cocoabeans.commands.CommandManager; -import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; public interface ArgumentRequirementFactory { + /** + * Create an argument requirement from the given annotations + * @param node node + * @param annotations annotations + * @param argumentRequirementFactories argument requirement factories + * @return argument requirements + */ + @ApiStatus.AvailableSince("0.0.37") + static ArgumentRequirement[] createArgumentRequirements(GenericNode node, Annotation[] annotations, Map, ArgumentRequirementFactory> argumentRequirementFactories) { + if (annotations == null) + return new ArgumentRequirement[0]; + + List result = new ArrayList<>(); + + for (Annotation annotation : annotations) { + Class argumentRequirementType = ArgumentRequirementFactory.getArgumentRequirementFactoryClass(annotation); + + if (argumentRequirementType == null) + continue; + + ArgumentRequirement argumentRequirement = Optional.ofNullable(argumentRequirementFactories.computeIfAbsent( + argumentRequirementType, + clazz -> ArgumentRequirementFactory.createFromAnnotation(annotation) + )) + .map(factory -> factory.getArgumentRequirement(node, annotation)) + .orElse(null); + + if (argumentRequirement == null) + continue; + + result.add(argumentRequirement); + } + + return result.toArray(new ArgumentRequirement[0]); + + } + /** * Get the class of the argument requirement factory * @param annotation annotation that has a ArgumentRequirementType @@ -38,11 +78,10 @@ static Class getArgumentRequirementFactory /** * Create an argument requirement factory * @param annotation annotation - * @param commandManager command manager * @return argument requirement factory */ @ApiStatus.AvailableSince("0.0.37") - static ArgumentRequirementFactory createFromAnnotation(Annotation annotation, CommandManager commandManager) { + static ArgumentRequirementFactory createFromAnnotation(Annotation annotation) { if (annotation == null) return null; @@ -56,12 +95,6 @@ static ArgumentRequirementFactory createFromAnnotation(Annotation annotation, Co if (constructor.getParameterCount() == 0) return constructor.newInstance(); - if (constructor.getParameters().length == 1 && constructor.getParameterTypes()[0].equals(CommandManager.class)) { - if (commandManager == null) - throw new IllegalArgumentException("Command manager cannot be null"); - return constructor.newInstance(commandManager); - } - return null; } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException("Failed to instantiate argument requirement factory: " + clazz, e); @@ -75,6 +108,6 @@ static ArgumentRequirementFactory createFromAnnotation(Annotation annotation, Co * @return argument requirement or null */ @Nullable - ArgumentRequirement getArgumentRequirement(CommandNode commandNode, Object obj); + ArgumentRequirement getArgumentRequirement(GenericNode commandNode, Object obj); } diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/RequirementFactory.java b/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/RequirementFactory.java index 888a3e079..c1056ffbc 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/RequirementFactory.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/RequirementFactory.java @@ -10,15 +10,60 @@ package net.apartium.cocoabeans.commands.requirements; -import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; public interface RequirementFactory { + /** + * Create a requirement set from the given annotations + * @param node generic node (command node or compound parser) + * @param annotations annotations + * @param requirementFactories requirement factories for caching factories + * @return requirement set + */ + @ApiStatus.AvailableSince("0.0.37") + static RequirementSet createRequirementSet(GenericNode node, Annotation[] annotations, Map, RequirementFactory> requirementFactories) { + if (annotations == null) + return new RequirementSet(); + + Set result = new HashSet<>(); + for (Annotation annotation : annotations) { + Requirement requirement = getRequirement(node, annotation, requirementFactories); + if (requirement == null) + continue; + + result.add(requirement); + } + + return new RequirementSet(result); + } + + @ApiStatus.Internal + private static Requirement getRequirement(GenericNode node, Annotation annotation, Map, RequirementFactory> requirementFactories) { + Class requirementFactoryClass = RequirementFactory.getRequirementFactoryClass(annotation); + + if (requirementFactoryClass == null) + return null; + + RequirementFactory requirementFactory = requirementFactories.computeIfAbsent( + requirementFactoryClass, + clazz -> RequirementFactory.createFromAnnotation(annotation) + ); + + if (requirementFactory == null) + return null; + + return requirementFactory.getRequirement(node, annotation); + } + /** * Get the class of the requirement factory * @param annotation annotation that has a CommandRequirementType @@ -57,12 +102,12 @@ static RequirementFactory createFromAnnotation(Annotation annotation) { /** * Create a requirement from the given object - * @param commandNode command node + * @param node generic node (command node or compound parser) * @param obj object * @return requirement or null */ @Nullable - Requirement getRequirement(CommandNode commandNode, Object obj); + Requirement getRequirement(GenericNode node, Object obj); } diff --git a/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/argument/RangeArgumentRequirementFactory.java b/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/argument/RangeArgumentRequirementFactory.java index c3574b8f6..6cf23f422 100644 --- a/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/argument/RangeArgumentRequirementFactory.java +++ b/commands/src/main/java/net/apartium/cocoabeans/commands/requirements/argument/RangeArgumentRequirementFactory.java @@ -11,7 +11,7 @@ package net.apartium.cocoabeans.commands.requirements.argument; import net.apartium.cocoabeans.commands.CommandContext; -import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.commands.Sender; import net.apartium.cocoabeans.commands.requirements.ArgumentRequirement; import net.apartium.cocoabeans.commands.requirements.ArgumentRequirementFactory; @@ -21,7 +21,7 @@ public class RangeArgumentRequirementFactory implements ArgumentRequirementFactory { @Override - public @Nullable ArgumentRequirement getArgumentRequirement(CommandNode commandNode, Object obj) { + public @Nullable ArgumentRequirement getArgumentRequirement(GenericNode node, Object obj) { if (obj == null) return null; @@ -39,10 +39,11 @@ public boolean meetsRequirement(@NotNull Sender sender, @Nullable CommandContext if (argument == null) return false; - if (!(argument instanceof Double)) + if (!(argument instanceof Number number)) return false; - double num = (double) argument; + + double num = number.doubleValue(); if (num < from) return false; diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/GeneralCommandTest.java b/commands/src/test/java/net/apartium/cocoabeans/commands/GeneralCommandTest.java index 2b48a22b6..b064672be 100644 --- a/commands/src/test/java/net/apartium/cocoabeans/commands/GeneralCommandTest.java +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/GeneralCommandTest.java @@ -115,7 +115,7 @@ void testingExample() { @Test void senderMeetsRequirementTest() { - CommandProcessingContext processingContext = new AbstractCommandProcessingContext(sender, "test", new String[0], 0); + CommandProcessingContext processingContext = new SimpleCommandProcessingContext(sender, "test", new String[0], 0); assertTrue(processingContext.senderMeetsRequirement(sender -> RequirementResult.meet()).meetRequirement()); assertFalse(processingContext.senderMeetsRequirement(sender -> RequirementResult.error(new UnmetRequirementResponse(null, null, null, 0, "no", null))).meetRequirement()); } @@ -345,6 +345,19 @@ void yesMeow() { assertEquals(List.of("fallbackHandle(Sender sender, String label, String[] args) You can't access that method... args: [yes, meOw]"), sender.getMessages()); } + @Test + void performanceTime() { + long start, end; + + for (int i = 0; i < 16_384; i++) { + start = System.currentTimeMillis(); + evaluate("test", "try random-string 0.3"); + end = System.currentTimeMillis(); + assertEquals(List.of("tryDouble(Sender sender, double num) I ignore the second argument it wasn't important also your number is 0.3"), sender.getMessages()); + assertTrue(end - start < 50); + } + } + @Test void stringsTest() { evaluate("test", "send hey"); @@ -723,6 +736,8 @@ void evilCommand() { assertEquals(List.of("Invalid usage"), sender.getMessages()); assertThrowsExactly(NoSuchElementException.class, () -> testCommandManager.addCommand(new MoreEvilCommand())); + + assertThrowsExactly(IllegalArgumentException.class, () -> testCommandManager.addCommand(new ThereAnotherEvilCommand())); } diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/RegisteredVariantTest.java b/commands/src/test/java/net/apartium/cocoabeans/commands/RegisteredVariantTest.java new file mode 100644 index 000000000..c659e03a9 --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/RegisteredVariantTest.java @@ -0,0 +1,43 @@ +package net.apartium.cocoabeans.commands; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class RegisteredVariantTest { + + @Test + void equalsTest() { + assertEquals(new RegisteredVariant( + null, + new RegisteredVariant.Parameter[0], + null, + List.of(), + 0 + ), new RegisteredVariant( + null, + new RegisteredVariant.Parameter[0], + null, + List.of(), + 0 + )); + + assertNotEquals(new RegisteredVariant( + null, + new RegisteredVariant.Parameter[0], + null, + List.of(), + 6 + ), new RegisteredVariant( + null, + new RegisteredVariant.Parameter[0], + null, + List.of(), + 0 + )); + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/ThereAnotherEvilCommand.java b/commands/src/test/java/net/apartium/cocoabeans/commands/ThereAnotherEvilCommand.java new file mode 100644 index 000000000..a2eed951a --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/ThereAnotherEvilCommand.java @@ -0,0 +1,11 @@ +package net.apartium.cocoabeans.commands; + +@Command("evil-lord") +public class ThereAnotherEvilCommand implements CommandNode { + + @SubCommand("") + public void meow(Sender sender, @Param("") int num) { + throw new UnsupportedOperationException("how did we get here"); + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/WallRequirementFactory.java b/commands/src/test/java/net/apartium/cocoabeans/commands/WallRequirementFactory.java index 9d711a02f..c0677024a 100644 --- a/commands/src/test/java/net/apartium/cocoabeans/commands/WallRequirementFactory.java +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/WallRequirementFactory.java @@ -15,7 +15,7 @@ public class WallRequirementFactory implements RequirementFactory { @Override - public Requirement getRequirement(CommandNode commandNode, Object obj) { + public Requirement getRequirement(GenericNode commandNode, Object obj) { return new WallRequirementImpl((WallRequirement) obj); } diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/multilayered/PermissionFactory.java b/commands/src/test/java/net/apartium/cocoabeans/commands/multilayered/PermissionFactory.java index be3d02a6b..436f5c126 100644 --- a/commands/src/test/java/net/apartium/cocoabeans/commands/multilayered/PermissionFactory.java +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/multilayered/PermissionFactory.java @@ -1,6 +1,6 @@ package net.apartium.cocoabeans.commands.multilayered; -import net.apartium.cocoabeans.commands.CommandNode; +import net.apartium.cocoabeans.commands.GenericNode; import net.apartium.cocoabeans.commands.Sender; import net.apartium.cocoabeans.commands.TestSender; import net.apartium.cocoabeans.commands.requirements.*; @@ -9,7 +9,7 @@ public class PermissionFactory implements RequirementFactory { @Override - public Requirement getRequirement(CommandNode commandNode, Object obj) { + public Requirement getRequirement(GenericNode node, Object obj) { if (obj == null) return null; diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/AnotherEvilCompoundParser.java b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/AnotherEvilCompoundParser.java new file mode 100644 index 000000000..eb94eb5f3 --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/AnotherEvilCompoundParser.java @@ -0,0 +1,19 @@ +package net.apartium.cocoabeans.commands.parsers.compound; + +import net.apartium.cocoabeans.commands.SimpleArgumentMapper; +import net.apartium.cocoabeans.commands.lexer.SimpleCommandLexer; +import net.apartium.cocoabeans.commands.parsers.CompoundParser; +import net.apartium.cocoabeans.commands.parsers.ParserVariant; + +public class AnotherEvilCompoundParser extends CompoundParser { + + public AnotherEvilCompoundParser(String keyword, int priority) { + super(keyword, String.class, priority, new SimpleArgumentMapper(), new SimpleCommandLexer()); + } + + @ParserVariant("ok") + public String toOk() { + return "ok"; + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/EvilCompoundParser.java b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/EvilCompoundParser.java new file mode 100644 index 000000000..a248172b5 --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/EvilCompoundParser.java @@ -0,0 +1,60 @@ +package net.apartium.cocoabeans.commands.parsers.compound; + +import net.apartium.cocoabeans.commands.RegisterArgumentParser; +import net.apartium.cocoabeans.commands.SimpleArgumentMapper; +import net.apartium.cocoabeans.commands.lexer.ArgumentParserToken; +import net.apartium.cocoabeans.commands.lexer.SimpleCommandLexer; +import net.apartium.cocoabeans.commands.lexer.SimpleKeywordToken; +import net.apartium.cocoabeans.commands.parsers.ArgumentParser; +import net.apartium.cocoabeans.commands.parsers.CompoundParser; +import net.apartium.cocoabeans.commands.parsers.ParserVariant; + +import java.time.Instant; +import java.util.Map; +import java.util.Optional; + +public class EvilCompoundParser extends CompoundParser { + + protected EvilCompoundParser(String keyword, int priority) { + super(keyword, Instant.class, priority, new SimpleArgumentMapper(), new SimpleCommandLexer(BasicArgumentParserToken::new, SimpleKeywordToken::new)); + } + + @ParserVariant("") + public Instant evilNoParserAhah(long time) { + return Instant.ofEpochMilli(time); + + } + + private static class BasicArgumentParserToken extends ArgumentParserToken { + + protected BasicArgumentParserToken(int from, int to, String text) { + super(from, to, text); + } + + @Override + public RegisterArgumentParser getParser(Map> parsers) { + return null; + } + + @Override + public String getParserName() { + return ""; + } + + @Override + public Optional getParameterName() { + return Optional.empty(); + } + + @Override + public boolean isOptional() { + return false; + } + + @Override + public boolean optionalNotMatch() { + return false; + } + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/EvilCompoundParserTest.java b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/EvilCompoundParserTest.java new file mode 100644 index 000000000..4f7a397b4 --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/EvilCompoundParserTest.java @@ -0,0 +1,15 @@ +package net.apartium.cocoabeans.commands.parsers.compound; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +class EvilCompoundParserTest { + + @Test + void creatingInstance() { + assertThrowsExactly(IllegalArgumentException.class, () -> new EvilCompoundParser("evil", 0)); + assertThrowsExactly(UnsupportedOperationException.class, () -> new AnotherEvilCompoundParser("evil", 0)); + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PersonCompoundParser.java b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PersonCompoundParser.java new file mode 100644 index 000000000..88c7c5663 --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PersonCompoundParser.java @@ -0,0 +1,32 @@ +package net.apartium.cocoabeans.commands.parsers.compound; + +import net.apartium.cocoabeans.commands.SimpleArgumentMapper; +import net.apartium.cocoabeans.commands.lexer.SimpleCommandLexer; +import net.apartium.cocoabeans.commands.parsers.*; +import net.apartium.cocoabeans.commands.requirements.argument.Range; + +@WithParser(IntParser.class) +@WithParser(StringParser.class) +public class PersonCompoundParser extends CompoundParser { + + public PersonCompoundParser(String keyword, int priority) { + super(keyword, Person.class, priority, new SimpleArgumentMapper(), new SimpleCommandLexer()); + } + + public PersonCompoundParser(int priority) { + this("person", priority); + } + + @ParserVariant(" ") + public Person toPerson(String name, @Range(to=120) int age) { + return new Person(name, age); + } + + public record Person( + String name, + int age + ) { + + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PersonCompoundParserTest.java b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PersonCompoundParserTest.java new file mode 100644 index 000000000..dff8228ce --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PersonCompoundParserTest.java @@ -0,0 +1,43 @@ +package net.apartium.cocoabeans.commands.parsers.compound; + +import net.apartium.cocoabeans.commands.exception.BadCommandResponse; +import net.apartium.cocoabeans.commands.parsers.ArgumentParser; +import net.apartium.cocoabeans.commands.parsers.ParserAssertions; +import org.junit.jupiter.api.Test; + +class PersonCompoundParserTest { + + @Test + void testingParser() { + PersonCompoundParser personParser = new PersonCompoundParser(0); + + ParserAssertions.assertParserResult( + personParser, + null, + null, + args("kfir 18"), + new ArgumentParser.ParseResult<>(new PersonCompoundParser.Person("kfir", 18), 2) + ); + + ParserAssertions.assertParserResult( + personParser, + null, + null, + args("tom 26"), + new ArgumentParser.ParseResult<>(new PersonCompoundParser.Person("tom", 26), 2) + ); + + ParserAssertions.assertParserThrowsReport( + personParser, + null, + null, + args("no 152"), + BadCommandResponse.class + ); + } + + static String[] args(String args) { + return args.split("\\s+"); + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PositionParser.java b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PositionParser.java new file mode 100644 index 000000000..bb55407ac --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PositionParser.java @@ -0,0 +1,33 @@ +package net.apartium.cocoabeans.commands.parsers.compound; + +import net.apartium.cocoabeans.commands.SimpleArgumentMapper; +import net.apartium.cocoabeans.commands.lexer.SimpleCommandLexer; +import net.apartium.cocoabeans.commands.parsers.*; +import net.apartium.cocoabeans.space.Position; + +public class PositionParser extends CompoundParser { + + public static final String DEFAULT_KEYWORD = "position"; + + public PositionParser(int priority, String keyword) { + super(keyword, Position.class, priority, new SimpleArgumentMapper(), new SimpleCommandLexer()); + } + + public PositionParser(int priority) { + this(priority, DEFAULT_KEYWORD); + } + + @WithParser(DoubleParser.class) + @ParserVariant(" ") + public Position pos(double x, double y, double z) { + return new Position(x, y, z); + } + + @WithParser(DoubleParser.class) + @WithParser(value = IntParser.class, priority = 1) + @ParserVariant(" ") + public Position pos(int x, double y, int z) { + return new Position(x * 5, y, z); + } + +} diff --git a/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PositionParserTest.java b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PositionParserTest.java new file mode 100644 index 000000000..1faff900f --- /dev/null +++ b/commands/src/test/java/net/apartium/cocoabeans/commands/parsers/compound/PositionParserTest.java @@ -0,0 +1,49 @@ +package net.apartium.cocoabeans.commands.parsers.compound; + +import net.apartium.cocoabeans.commands.exception.BadCommandResponse; +import net.apartium.cocoabeans.commands.parsers.ArgumentParser; +import net.apartium.cocoabeans.commands.parsers.ParserAssertions; +import net.apartium.cocoabeans.space.Position; +import org.junit.jupiter.api.Test; + +import java.util.OptionalInt; +import java.util.Set; + +class PositionParserTest { + + @Test + void parse() { + PositionParser parser = new PositionParser(0); + + ParserAssertions.assertParserResult(parser, null, null, args("0 0 0"), new ArgumentParser.ParseResult<>(new Position(0, 0, 0), 3)); + ParserAssertions.assertParserResult(parser, null, null, args("1.0 2 3"), new ArgumentParser.ParseResult<>(new Position(1, 2, 3), 3)); + ParserAssertions.assertParserResult(parser, null, null, args("1 2 36.2"), new ArgumentParser.ParseResult<>(new Position(1, 2, 36.2), 3)); + + ParserAssertions.assertParserResult(parser, null, null, args("test 0 0 0"), 1, new ArgumentParser.ParseResult<>(new Position(0, 0, 0), 4)); + ParserAssertions.assertParserResult(parser, null, null, args("set cat 0 0 0"), 2, new ArgumentParser.ParseResult<>(new Position(0, 0, 0), 5)); + + ParserAssertions.assertParserResult(parser, null, null, args("set cat 0 0 0 wow"), 2, new ArgumentParser.ParseResult<>(new Position(0, 0, 0), 5)); + + ParserAssertions.assertParserResult(parser, null, null, args("1 2.0 3"), new ArgumentParser.ParseResult<>(new Position(5, 2, 3), 3)); + + ParserAssertions.assertParserThrowsReport(parser, null, null, args("1"), BadCommandResponse.class); + ParserAssertions.assertParserThrowsReport(parser, null, null, args("asd 13 31"), BadCommandResponse.class); + + ParserAssertions.assertTryParseResult(parser, null, null, args("3 5 2"), OptionalInt.of(3)); + ParserAssertions.assertTryParseResult(parser, null, null, args("meow 3 5 2"), 1, OptionalInt.of(4)); + ParserAssertions.assertTryParseResult(parser, null, null, args("2 1"), OptionalInt.empty()); + } + + @Test + void tabCompletion() { + PositionParser parser = new PositionParser(0); + + ParserAssertions.assertParserTabCompletion(parser, null, null, args("1"), 0, Set.of("10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1."), 1); + ParserAssertions.assertParserTabCompletion(parser, null, null, args("1 6 -"), 2, Set.of("-.", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9"), 3); + } + + + private String[] args(String args) { + return args.split("\\s+"); + } +} diff --git a/commands/src/testFixtures/java/net/apartium/cocoabeans/commands/parsers/ParserAssertions.java b/commands/src/testFixtures/java/net/apartium/cocoabeans/commands/parsers/ParserAssertions.java index efdfe0e0c..f9725533c 100644 --- a/commands/src/testFixtures/java/net/apartium/cocoabeans/commands/parsers/ParserAssertions.java +++ b/commands/src/testFixtures/java/net/apartium/cocoabeans/commands/parsers/ParserAssertions.java @@ -9,10 +9,9 @@ import org.jetbrains.annotations.ApiStatus; import org.junit.jupiter.api.AssertionFailureBuilder; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.OptionalInt; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Parser assertions is a collection of assertions for parsers @@ -316,6 +315,57 @@ public static void assertParserThrowsReport(ArgumentParser parser, Sender sen failMessage("Reports aren't the same as expected", message, expected, reports); } + /** + * + * @param parser + * @param sender + * @param label + * @param args + * @param startIndex + * @param expected + */ + @ApiStatus.AvailableSince("0.0.37") + public static void assertParserTabCompletion(ArgumentParser parser, Sender sender, String label, String[] args, int startIndex, Set expected, int expectedIndex) { + assertParserTabCompletion(parser, sender, label, args, startIndex, expected, expectedIndex, null); + } + + /** + * + * @param parser + * @param sender + * @param label + * @param args + * @param startIndex + * @param expected + * @param message + */ + @ApiStatus.AvailableSince("0.0.37") + public static void assertParserTabCompletion(ArgumentParser parser, Sender sender, String label, String[] args, int startIndex, Set expected, int expectedIndex, String message) { + TestCommandProcessingContext context = new TestCommandProcessingContext(sender, label, List.of(args), startIndex); + Optional tabCompletionResult = parser.tabCompletion(context); + + if (context.hasAnyReports()) { + List reports = context.getReports(); + failMessage("Parser produced unexpected error reports", message, expected, reports); + return; + } + + if (expected == null) { + if (tabCompletionResult.isPresent()) + failMessage("Expected no tab completion", message, null, tabCompletionResult.get().result()); + return; + } + + if (tabCompletionResult.isEmpty()) { + failMessage("Expected tab completion", message, expected, null); + return; + } + + Set result = tabCompletionResult.get().result(); + assertEquals(expected, result, message); + assertEquals(expectedIndex, tabCompletionResult.get().newIndex(), message); + } + @ApiStatus.Internal private static void failMessage(String reason, String message, Object expected, Object actual) { AssertionFailureBuilder.assertionFailure().reason(reason).message(message).expected(expected).actual(actual).buildAndThrow();