From 29f5123f0118cb6070b8e427a1a21df1182fca98 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Sun, 4 Jan 2026 20:51:16 +0100 Subject: [PATCH 1/5] feat(core): add wrapper and primitive type in internal converters --- .../java/fr/traqueur/commands/api/CommandManager.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java index a3782e2..73d9560 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -465,9 +465,18 @@ public CommandInvoker getInvoker() { */ private void registerInternalConverters() { this.registerConverter(String.class, (s) -> s); + + // Register both primitive and wrapper types for DefaultArgumentParser (Spigot/Velocity). + // JDA's ArgumentParser handles primitives internally, but text-based platforms need explicit registration. + // Wrapper types (Integer.class, Long.class, etc.) are registered for compatibility with wrapper usage. + // Primitive types (int.class, long.class, etc.) are registered to support primitive method parameters. this.registerConverter(Boolean.class, new BooleanArgument<>()); + this.registerConverter(boolean.class, new BooleanArgument<>()); this.registerConverter(Integer.class, new IntegerArgument()); + this.registerConverter(int.class, new IntegerArgument()); this.registerConverter(Double.class, new DoubleArgument()); + this.registerConverter(double.class, new DoubleArgument()); this.registerConverter(Long.class, new LongArgument()); + this.registerConverter(long.class, new LongArgument()); } } From ee31c9c278252d509fb9a74cf62376765481012e Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Sun, 4 Jan 2026 20:52:11 +0100 Subject: [PATCH 2/5] feat(core): version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f81469e..8656a1e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.0.0 \ No newline at end of file +version=5.0.1 \ No newline at end of file From 0b12ab9d15c34b7e081f570291bf245598e0f43a Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Sun, 4 Jan 2026 21:01:04 +0100 Subject: [PATCH 3/5] feat(core&bom): add bom and fix converter --- bom/README.md | 96 +++++++++++++++++++ bom/build.gradle | 77 +++++++++++++++ build.gradle | 8 +- .../traqueur/commands/api/CommandManager.java | 2 +- .../commands/api/arguments/ArgumentType.java | 2 +- settings.gradle | 1 + 6 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 bom/README.md create mode 100644 bom/build.gradle diff --git a/bom/README.md b/bom/README.md new file mode 100644 index 0000000..723a30d --- /dev/null +++ b/bom/README.md @@ -0,0 +1,96 @@ +# CommandsAPI BOM (Bill of Materials) + +This module provides a BOM (Bill of Materials) for CommandsAPI, making it easier to manage consistent versions across all CommandsAPI modules. + +## Usage + +### Gradle (Kotlin DSL) + +```kotlin +dependencies { + // Import the BOM + implementation(platform("fr.traqueur.commands:bom:VERSION")) + + // Then add dependencies without specifying versions + implementation("fr.traqueur.commands:core") + implementation("fr.traqueur.commands:platform-spigot") + implementation("fr.traqueur.commands:platform-velocity") + implementation("fr.traqueur.commands:platform-jda") + implementation("fr.traqueur.commands:annotations-addon") +} +``` + +### Gradle (Groovy DSL) + +```groovy +dependencies { + // Import the BOM + implementation platform('fr.traqueur.commands:bom:VERSION') + + // Then add dependencies without specifying versions + implementation 'fr.traqueur.commands:core' + implementation 'fr.traqueur.commands:platform-spigot' + implementation 'fr.traqueur.commands:platform-velocity' + implementation 'fr.traqueur.commands:platform-jda' + implementation 'fr.traqueur.commands:annotations-addon' +} +``` + +### Maven + +```xml + + + + fr.traqueur.commands + bom + VERSION + pom + import + + + + + + + + fr.traqueur.commands + core + + + fr.traqueur.commands + platform-spigot + + + fr.traqueur.commands + platform-velocity + + + fr.traqueur.commands + platform-jda + + + fr.traqueur.commands + annotations-addon + + +``` + +## Benefits + +Using the BOM provides several advantages: + +1. **Version Consistency**: All CommandsAPI modules will use compatible versions +2. **Simplified Dependency Management**: No need to specify versions for each module +3. **Easier Updates**: Update all modules by changing only the BOM version +4. **Reduced Conflicts**: Ensures all modules work together correctly + +## Available Modules + +The BOM manages versions for the following modules: + +- `core` - Core functionality and API +- `platform-spigot` - Spigot/Bukkit platform support +- `platform-velocity` - Velocity proxy platform support +- `platform-jda` - JDA (Discord) platform support +- `annotations-addon` - Annotation-based command registration \ No newline at end of file diff --git a/bom/build.gradle b/bom/build.gradle new file mode 100644 index 0000000..cc95d7b --- /dev/null +++ b/bom/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'java-platform' + id 'maven-publish' +} + +description = 'CommandsAPI BOM (Bill of Materials)' + +javaPlatform { + allowDependencies() +} + +dependencies { + constraints { + api(project(':core')) + api(project(':spigot')) + api(project(':velocity')) + api(project(':jda')) + api(project(':annotations-addon')) + } +} + +publishing { + repositories { + maven { + def repository = System.getProperty('repository.name', 'snapshots') + def repoType = repository.toLowerCase() + + name = "groupez${repository.capitalize()}" + url = uri("https://repo.groupez.dev/${repoType}") + + credentials { + username = findProperty("${name}Username") ?: System.getenv('MAVEN_USERNAME') + password = findProperty("${name}Password") ?: System.getenv('MAVEN_PASSWORD') + } + + authentication { + create("basic", BasicAuthentication) + } + } + } + + publications { + create('maven', MavenPublication) { + from components.javaPlatform + + groupId = rootProject.group.toString() + artifactId = 'bom' + version = rootProject.version.toString() + + pom { + name = 'CommandsAPI BOM' + description = 'CommandsAPI Bill of Materials - Manages consistent versions across CommandsAPI modules' + url = 'https://github.com/Traqueur-dev/CommandsAPI' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + + developers { + developer { + id = 'traqueur' + name = 'Traqueur' + } + } + + scm { + connection = 'scm:git:git://github.com/Traqueur-dev/CommandsAPI.git' + developerConnection = 'scm:git:ssh://github.com/Traqueur-dev/CommandsAPI.git' + url = 'https://github.com/Traqueur-dev/CommandsAPI' + } + } + } + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index d2e5a4b..f6300d2 100644 --- a/build.gradle +++ b/build.gradle @@ -7,8 +7,10 @@ allprojects { group = 'fr.traqueur.commands' version = property('version') - apply { - plugin 'java-library' + if (project.name != 'bom') { + apply { + plugin 'java-library' + } } ext.classifier = System.getProperty('archive.classifier') @@ -27,7 +29,7 @@ allprojects { } subprojects { - if (project.name.contains('test-')) { + if (project.name.contains('test-') || project.name == 'bom') { return; } diff --git a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java index 73d9560..af9f7bb 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -202,7 +202,7 @@ public void unregisterCommand(Command command, boolean subcommands) { * @param The type of the argument. */ public void registerConverter(Class typeClass, ArgumentConverter converter) { - this.typeConverters.put(typeClass.getSimpleName().toLowerCase(), new ArgumentConverter.Wrapper<>(typeClass, converter)); + this.typeConverters.put(typeClass.getName().toLowerCase(), new ArgumentConverter.Wrapper<>(typeClass, converter)); } /** diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java index 55c958d..fc36b71 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java @@ -24,7 +24,7 @@ record Simple(Class clazz) implements ArgumentType { @Override public String key() { - return clazz.getSimpleName().toLowerCase(); + return clazz.getName().toLowerCase(); } } diff --git a/settings.gradle b/settings.gradle index 749e16c..564065b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'CommandsAPI' +include 'bom' include 'velocity-test-plugin' include 'spigot-test-plugin' include 'spigot' From ae265e96454e71472c93cf8ff1f3a778c5668f40 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Sun, 4 Jan 2026 21:19:41 +0100 Subject: [PATCH 4/5] feat(core): better management of arguments --- .../traqueur/commands/api/CommandManager.java | 14 ++--- .../commands/api/arguments/Argument.java | 2 +- .../commands/api/arguments/ArgumentType.java | 10 ++-- .../impl/parsing/DefaultArgumentParser.java | 8 +-- .../parsing/DefaultArgumentParserTest.java | 8 +-- .../fr/traqueur/commands/jda/JDAPlatform.java | 58 +++++++++++++++---- 6 files changed, 69 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java index af9f7bb..3dcab35 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -49,7 +49,7 @@ public abstract class CommandManager { /** * The argument converters registered in the command manager. */ - private final Map> typeConverters; + private final Map, ArgumentConverter.Wrapper> typeConverters; /** * The tab completer registered in the command manager. @@ -202,7 +202,7 @@ public void unregisterCommand(Command command, boolean subcommands) { * @param The type of the argument. */ public void registerConverter(Class typeClass, ArgumentConverter converter) { - this.typeConverters.put(typeClass.getName().toLowerCase(), new ArgumentConverter.Wrapper<>(typeClass, converter)); + this.typeConverters.put(typeClass, new ArgumentConverter.Wrapper<>(typeClass, converter)); } /** @@ -251,8 +251,8 @@ public Map>> getCompleters() { * @param type The type to check. * @return true if a TabCompleter is registered for this type. */ - public boolean hasTabCompleterForType(String type) { - ArgumentConverter.Wrapper wrapper = this.typeConverters.get(type.toLowerCase()); + public boolean hasTabCompleterForType(Class type) { + ArgumentConverter.Wrapper wrapper = this.typeConverters.get(type); return wrapper != null && wrapper.converter() instanceof TabCompleter; } @@ -263,8 +263,8 @@ public boolean hasTabCompleterForType(String type) { * @return The TabCompleter for this type, or null if none exists. */ @SuppressWarnings("unchecked") - public TabCompleter getTabCompleterForType(String type) { - ArgumentConverter.Wrapper wrapper = this.typeConverters.get(type.toLowerCase()); + public TabCompleter getTabCompleterForType(Class type) { + ArgumentConverter.Wrapper wrapper = this.typeConverters.get(type); if (wrapper != null && wrapper.converter() instanceof TabCompleter) { return (TabCompleter) wrapper.converter(); } @@ -411,7 +411,7 @@ private void addCompletionsForLabel(String[] labelParts) { private void addCompletionForArgs(String label, int commandSize, List> args) { for (int i = 0; i < args.size(); i++) { Argument arg = args.get(i); - String type = arg.type().key(); + Class type = arg.type().key(); ArgumentConverter.Wrapper entry = this.typeConverters.get(type); TabCompleter argConverter = arg.tabCompleter(); if (argConverter != null) { diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java index a287103..85d8862 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java @@ -43,7 +43,7 @@ public Argument(String name, ArgumentType type) { } public String canonicalName() { - return this.name + ":" + this.type.key(); + return this.name + ":" + this.type.key().getSimpleName().toLowerCase(); } diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java index fc36b71..6ece82e 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java @@ -9,7 +9,7 @@ static ArgumentType of(Class clazz) { return new Simple(clazz); } - String key(); + Class key(); /** * Check if this is the infinite type. @@ -23,8 +23,8 @@ default boolean isInfinite() { record Simple(Class clazz) implements ArgumentType { @Override - public String key() { - return clazz.getName().toLowerCase(); + public Class key() { + return clazz; } } @@ -33,8 +33,8 @@ record Infinite() implements ArgumentType { public static final Infinite INSTANCE = new Infinite(); @Override - public String key() { - return "infinite"; + public Class key() { + return fr.traqueur.commands.api.arguments.Infinite.class; } } diff --git a/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java index 1abfdfc..9f66e4a 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java +++ b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java @@ -20,10 +20,10 @@ public class DefaultArgumentParser implements ArgumentParser> typeConverters; + private final Map, ArgumentConverter.Wrapper> typeConverters; private final Logger logger; - public DefaultArgumentParser(Map> typeConverters, Logger logger) { + public DefaultArgumentParser(Map, ArgumentConverter.Wrapper> typeConverters, Logger logger) { this.typeConverters = typeConverters; this.logger = logger; } @@ -80,7 +80,7 @@ public ParseResult parse(Command command, String[] rawArgs) { } private ParseResult parseSingle(Arguments arguments, Argument arg, String input) { - String typeKey = arg.type().key(); + Class typeKey = arg.type().key(); ArgumentConverter.Wrapper wrapper = typeConverters.get(typeKey); if (wrapper == null) { @@ -88,7 +88,7 @@ private ParseResult parseSingle(Arguments arguments, Argument arg, String inp ParseError.Type.TYPE_NOT_FOUND, arg.name(), input, - "No converter for type: " + typeKey + "No converter for type: " + typeKey.getSimpleName() )); } diff --git a/core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java b/core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java index fb2fcc4..d76a38e 100644 --- a/core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java +++ b/core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java @@ -22,16 +22,16 @@ class DefaultArgumentParserTest { @BeforeEach void setUp() { - Map> converters = new HashMap<>(); - converters.put("string", new ArgumentConverter.Wrapper<>(String.class, s -> s)); - converters.put("integer", new ArgumentConverter.Wrapper<>(Integer.class, s -> { + Map, ArgumentConverter.Wrapper> converters = new HashMap<>(); + converters.put(String.class, new ArgumentConverter.Wrapper<>(String.class, s -> s)); + converters.put(Integer.class, new ArgumentConverter.Wrapper<>(Integer.class, s -> { try { return Integer.valueOf(s); } catch (NumberFormatException e) { return null; } })); - converters.put("double", new ArgumentConverter.Wrapper<>(Double.class, s -> { + converters.put(Double.class, new ArgumentConverter.Wrapper<>(Double.class, s -> { try { return Double.valueOf(s); } catch (NumberFormatException e) { diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java index 7d97aeb..341b29d 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java @@ -7,6 +7,8 @@ import fr.traqueur.commands.api.resolver.SenderResolver; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.*; @@ -288,20 +290,56 @@ private boolean hasTabCompleter(Argument arg) { * @param type The type string. * @return The corresponding OptionType. */ - private OptionType mapToOptionType(String type) { - return switch (type.toLowerCase()) { - case "integer", "int", "long" -> OptionType.INTEGER; - case "boolean" -> OptionType.BOOLEAN; - case "user", "member" -> OptionType.USER; - case "role" -> OptionType.ROLE; - case "channel", "guildchannelunion" -> OptionType.CHANNEL; - case "double", "float" -> OptionType.NUMBER; - case "attachment" -> OptionType.ATTACHMENT; - case "mentionable" -> OptionType.MENTIONABLE; + private OptionType mapToOptionType(Class type) { + return switch (type) { + + // INTEGER + case Class t when t == int.class + || t == Integer.class + || t == long.class + || t == Long.class + -> OptionType.INTEGER; + + // BOOLEAN + case Class t when t == boolean.class + || t == Boolean.class + -> OptionType.BOOLEAN; + + // USER / MEMBER + case Class t when User.class.isAssignableFrom(t) + || Member.class.isAssignableFrom(t) + -> OptionType.USER; + + // ROLE + case Class t when Role.class.isAssignableFrom(t) + -> OptionType.ROLE; + + // CHANNEL (tous types de channels) + case Class t when GuildChannel.class.isAssignableFrom(t) + -> OptionType.CHANNEL; + + // NUMBER + case Class t when t == double.class + || t == Double.class + || t == float.class + || t == Float.class + -> OptionType.NUMBER; + + // ATTACHMENT + case Class t when Message.Attachment.class.isAssignableFrom(t) + -> OptionType.ATTACHMENT; + + // MENTIONABLE + case Class t when IMentionable.class.isAssignableFrom(t) + -> OptionType.MENTIONABLE; + default -> OptionType.STRING; }; } + + + /** * Synchronize all registered commands with Discord globally. * This may take up to 1 hour to update. From c931718839889bc5b0f826ce1352eeaab9b0a4e6 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Sun, 4 Jan 2026 21:49:59 +0100 Subject: [PATCH 5/5] feat(core): better encapsulation of primitve type --- .../AnnotationCommandProcessorTest.java | 88 ++++++- .../PrimitiveTypesIntegrationTest.java | 225 ++++++++++++++++++ .../commands/PrimitiveTypesTestCommands.java | 53 +++++ .../api/arguments/ArgumentConverter.java | 31 ++- gradle.properties | 2 +- 5 files changed, 394 insertions(+), 5 deletions(-) create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/PrimitiveTypesIntegrationTest.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/PrimitiveTypesTestCommands.java diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java index e548aac..cd3ddc9 100644 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java @@ -387,15 +387,97 @@ void commandsShouldBeSortedByDepth() { processor.register(commands); List labels = platform.getRegisteredLabels(); - + int adminIndex = labels.indexOf("admin"); int adminReloadIndex = labels.indexOf("admin.reload"); int adminReloadConfigIndex = labels.indexOf("admin.reload.config"); - - assertTrue(adminIndex < adminReloadIndex, + + assertTrue(adminIndex < adminReloadIndex, "admin should be registered before admin.reload"); assertTrue(adminReloadIndex < adminReloadConfigIndex, "admin.reload should be registered before admin.reload.config"); } } + + @Nested + @DisplayName("Primitive Types Support") + class PrimitiveTypesSupport { + + @Test + @DisplayName("should register command with primitive int argument") + void shouldRegisterCommandWithPrimitiveInt() { + PrimitiveTypesTestCommands commands = new PrimitiveTypesTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("primitiveint")); + Command cmd = platform.getCommand("primitiveint"); + assertNotNull(cmd); + + List> args = cmd.getArgs(); + assertEquals(1, args.size()); + assertEquals("value", args.get(0).name()); + } + + @Test + @DisplayName("should register command with primitive long argument") + void shouldRegisterCommandWithPrimitiveLong() { + PrimitiveTypesTestCommands commands = new PrimitiveTypesTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("primitivelong")); + Command cmd = platform.getCommand("primitivelong"); + assertNotNull(cmd); + + List> args = cmd.getArgs(); + assertEquals(1, args.size()); + assertEquals("value", args.get(0).name()); + } + + @Test + @DisplayName("should register command with primitive double argument") + void shouldRegisterCommandWithPrimitiveDouble() { + PrimitiveTypesTestCommands commands = new PrimitiveTypesTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("primitivedouble")); + Command cmd = platform.getCommand("primitivedouble"); + assertNotNull(cmd); + + List> args = cmd.getArgs(); + assertEquals(1, args.size()); + assertEquals("value", args.get(0).name()); + } + + @Test + @DisplayName("should register command with primitive boolean argument") + void shouldRegisterCommandWithPrimitiveBoolean() { + PrimitiveTypesTestCommands commands = new PrimitiveTypesTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("primitivebool")); + Command cmd = platform.getCommand("primitivebool"); + assertNotNull(cmd); + + List> args = cmd.getArgs(); + assertEquals(1, args.size()); + assertEquals("enabled", args.get(0).name()); + } + + @Test + @DisplayName("should register command with mixed primitive types") + void shouldRegisterCommandWithMixedPrimitives() { + PrimitiveTypesTestCommands commands = new PrimitiveTypesTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("mixedprimitives")); + Command cmd = platform.getCommand("mixedprimitives"); + assertNotNull(cmd); + + List> args = cmd.getArgs(); + assertEquals(3, args.size()); + assertEquals("count", args.get(0).name()); + assertEquals("enabled", args.get(1).name()); + assertEquals("ratio", args.get(2).name()); + } + } } \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/PrimitiveTypesIntegrationTest.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/PrimitiveTypesIntegrationTest.java new file mode 100644 index 0000000..29b86c8 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/PrimitiveTypesIntegrationTest.java @@ -0,0 +1,225 @@ +package fr.traqueur.commands.annotations; + +import fr.traqueur.commands.annotations.commands.PrimitiveTypesTestCommands; +import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.test.mocks.MockCommandManager; +import fr.traqueur.commands.test.mocks.MockPlatform; +import fr.traqueur.commands.test.mocks.MockSender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for primitive types support. + * These tests verify that commands with primitive type arguments can be: + * 1. Parsed correctly + * 2. Stored in Arguments + * 3. Retrieved from Arguments + * 4. Passed to method invocation via reflection + */ +@DisplayName("Primitive Types Integration") +class PrimitiveTypesIntegrationTest { + + private MockPlatform platform; + private MockCommandManager manager; + private PrimitiveTypesTestCommands commands; + + // Mock sender implementation for testing + private static class TestSender implements MockSender { + @Override + public void sendMessage(String message) { + // Do nothing + } + + @Override + public boolean hasPermission(String permission) { + return true; + } + } + + @BeforeEach + void setUp() { + platform = new MockPlatform(); + manager = new MockCommandManager(platform); + AnnotationCommandProcessor processor = new AnnotationCommandProcessor<>(manager); + commands = new PrimitiveTypesTestCommands(); + processor.register(commands); + } + + @Test + @DisplayName("should execute command with primitive int") + void shouldExecuteCommandWithPrimitiveInt() throws Exception { + Command cmd = platform.getCommand("primitiveint"); + assertNotNull(cmd); + + // Parse arguments + Arguments args = manager.parse(cmd, new String[]{"42"}); + assertNotNull(args); + assertEquals(1, args.size()); + + // Verify the argument was parsed and stored correctly + Integer value = args.get("value"); + assertEquals(42, value); + + // Execute the command + TestSender sender = new TestSender(); + cmd.execute(sender, args); + + // Verify the method was called + assertEquals(1, commands.executedCommands.size()); + assertEquals("primitiveint", commands.executedCommands.getFirst()); + + // Verify arguments were passed correctly + Object[] invokeArgs = commands.executedArgs.getFirst(); + assertEquals(2, invokeArgs.length); + assertEquals(sender, invokeArgs[0]); + assertEquals(42, invokeArgs[1]); + assertInstanceOf(Integer.class, invokeArgs[1]); + } + + @Test + @DisplayName("should execute command with primitive boolean") + void shouldExecuteCommandWithPrimitiveBoolean() throws Exception { + Command cmd = platform.getCommand("primitivebool"); + assertNotNull(cmd); + + // Parse arguments + Arguments args = manager.parse(cmd, new String[]{"true"}); + assertNotNull(args); + + // Verify the argument was parsed and stored correctly + Boolean enabled = args.get("enabled"); + assertTrue(enabled); + + // Execute the command + TestSender sender = new TestSender(); + cmd.execute(sender, args); + + // Verify the method was called correctly + assertEquals(1, commands.executedCommands.size()); + Object[] invokeArgs = commands.executedArgs.getFirst(); + assertEquals(true, invokeArgs[1]); + assertInstanceOf(Boolean.class, invokeArgs[1]); + } + + @Test + @DisplayName("should execute command with primitive double") + void shouldExecuteCommandWithPrimitiveDouble() throws Exception { + Command cmd = platform.getCommand("primitivedouble"); + assertNotNull(cmd); + + // Parse arguments + Arguments args = manager.parse(cmd, new String[]{"3.14"}); + assertNotNull(args); + + // Verify the argument was parsed and stored correctly + Double value = args.get("value"); + assertEquals(3.14, value, 0.001); + + // Execute the command + TestSender sender = new TestSender(); + cmd.execute(sender, args); + + // Verify the method was called correctly + assertEquals(1, commands.executedCommands.size()); + Object[] invokeArgs = commands.executedArgs.getFirst(); + assertInstanceOf(Double.class, invokeArgs[1]); + assertEquals(3.14, (Double) invokeArgs[1], 0.001); + } + + @Test + @DisplayName("should execute command with primitive long") + void shouldExecuteCommandWithPrimitiveLong() throws Exception { + Command cmd = platform.getCommand("primitivelong"); + assertNotNull(cmd); + + // Parse arguments + Arguments args = manager.parse(cmd, new String[]{"9223372036854775807"}); + assertNotNull(args); + + // Verify the argument was parsed and stored correctly + Long value = args.get("value"); + assertEquals(9223372036854775807L, value); + + // Execute the command + TestSender sender = new TestSender(); + cmd.execute(sender, args); + + // Verify the method was called correctly + assertEquals(1, commands.executedCommands.size()); + Object[] invokeArgs = commands.executedArgs.getFirst(); + assertEquals(9223372036854775807L, invokeArgs[1]); + assertInstanceOf(Long.class, invokeArgs[1]); + } + + @Test + @DisplayName("should execute command with mixed primitive types") + void shouldExecuteCommandWithMixedPrimitives() throws Exception { + Command cmd = platform.getCommand("mixedprimitives"); + assertNotNull(cmd); + + // Parse arguments + Arguments args = manager.parse(cmd, new String[]{"10", "true", "2.5"}); + assertNotNull(args); + assertEquals(3, args.size()); + + // Verify all arguments were parsed and stored correctly + Integer count = args.get("count"); + Boolean enabled = args.get("enabled"); + Double ratio = args.get("ratio"); + + assertEquals(10, count); + assertTrue(enabled); + assertEquals(2.5, ratio, 0.001); + + // Execute the command + TestSender sender = new TestSender(); + cmd.execute(sender, args); + + // Verify the method was called correctly with all arguments + assertEquals(1, commands.executedCommands.size()); + assertEquals("mixedprimitives", commands.executedCommands.getFirst()); + + Object[] invokeArgs = commands.executedArgs.getFirst(); + assertEquals(4, invokeArgs.length); + assertEquals(sender, invokeArgs[0]); + assertEquals(10, invokeArgs[1]); + assertEquals(true, invokeArgs[2]); + assertEquals(2.5, (Double) invokeArgs[3], 0.001); + } + + @Test + @DisplayName("should handle negative primitive int") + void shouldHandleNegativePrimitiveInt() throws Exception { + Command cmd = platform.getCommand("primitiveint"); + Arguments args = manager.parse(cmd, new String[]{"-42"}); + + Integer value = args.get("value"); + assertEquals(-42, value); + + TestSender sender = new TestSender(); + cmd.execute(sender, args); + + Object[] invokeArgs = commands.executedArgs.getFirst(); + assertEquals(-42, invokeArgs[1]); + } + + @Test + @DisplayName("should handle false boolean primitive") + void shouldHandleFalseBooleanPrimitive() throws Exception { + Command cmd = platform.getCommand("primitivebool"); + Arguments args = manager.parse(cmd, new String[]{"false"}); + + Boolean enabled = args.get("enabled"); + assertFalse(enabled); + + TestSender sender = new TestSender(); + cmd.execute(sender, args); + + Object[] invokeArgs = commands.executedArgs.getFirst(); + assertEquals(false, invokeArgs[1]); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/PrimitiveTypesTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/PrimitiveTypesTestCommands.java new file mode 100644 index 0000000..a425449 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/PrimitiveTypesTestCommands.java @@ -0,0 +1,53 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.test.mocks.MockSender; + +import java.util.ArrayList; +import java.util.List; + +/** + * Test commands using primitive types (int, boolean, double, long) instead of wrappers. + * This tests that the ArgumentConverter.Wrapper properly normalizes primitive types to wrappers. + */ +@CommandContainer +public class PrimitiveTypesTestCommands { + + public final List executedCommands = new ArrayList<>(); + public final List executedArgs = new ArrayList<>(); + + @Command(name = "primitiveint", description = "Test command with primitive int") + public void primitiveIntCommand(MockSender sender, @Arg("value") int value) { + executedCommands.add("primitiveint"); + executedArgs.add(new Object[]{sender, value}); + } + + @Command(name = "primitivelong", description = "Test command with primitive long") + public void primitiveLongCommand(MockSender sender, @Arg("value") long value) { + executedCommands.add("primitivelong"); + executedArgs.add(new Object[]{sender, value}); + } + + @Command(name = "primitivedouble", description = "Test command with primitive double") + public void primitiveDoubleCommand(MockSender sender, @Arg("value") double value) { + executedCommands.add("primitivedouble"); + executedArgs.add(new Object[]{sender, value}); + } + + @Command(name = "primitivebool", description = "Test command with primitive boolean") + public void primitiveBooleanCommand(MockSender sender, @Arg("enabled") boolean enabled) { + executedCommands.add("primitivebool"); + executedArgs.add(new Object[]{sender, enabled}); + } + + @Command(name = "mixedprimitives", description = "Test command with multiple primitive types") + public void mixedPrimitivesCommand(MockSender sender, + @Arg("count") int count, + @Arg("enabled") boolean enabled, + @Arg("ratio") double ratio) { + executedCommands.add("mixedprimitives"); + executedArgs.add(new Object[]{sender, count, enabled, ratio}); + } +} \ No newline at end of file diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java index 51d9990..22dc091 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java @@ -22,12 +22,41 @@ public interface ArgumentConverter extends Function { record Wrapper(Class clazz, ArgumentConverter converter) { + /** + * Convert primitive types to their wrapper equivalents. + * This is necessary because Arguments storage uses Class.isInstance(), + * which doesn't work with primitive types (they have no instances). + * + * @param type the type to normalize + * @return the wrapper type if input was primitive, otherwise the input type unchanged + */ + @SuppressWarnings("unchecked") + private static Class toWrapperType(Class type) { + if (!type.isPrimitive()) { + return type; + } + + if (type == int.class) return (Class) Integer.class; + if (type == long.class) return (Class) Long.class; + if (type == double.class) return (Class) Double.class; + if (type == float.class) return (Class) Float.class; + if (type == boolean.class) return (Class) Boolean.class; + if (type == byte.class) return (Class) Byte.class; + if (type == short.class) return (Class) Short.class; + if (type == char.class) return (Class) Character.class; + if (type == void.class) return (Class) Void.class; + + return type; + } + public boolean convertAndApply(String input, String name, Arguments arguments) { T result = converter.apply(input); if (result == null) { return false; } - arguments.add(name, clazz, result); + // Always store with wrapper type to ensure Class.isInstance() works correctly + Class storageType = toWrapperType(clazz); + arguments.add(name, storageType, result); return true; } diff --git a/gradle.properties b/gradle.properties index 8656a1e..b68a202 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=5.0.1 \ No newline at end of file +version=5.0.2 \ No newline at end of file