diff --git a/examples/bukkit-jakarta/build.gradle.kts b/examples/bukkit-jakarta/build.gradle.kts new file mode 100644 index 000000000..c746552d0 --- /dev/null +++ b/examples/bukkit-jakarta/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + id("java") + id("com.gradleup.shadow") version "9.3.1" + id("net.minecrell.plugin-yml.bukkit") version "0.6.0" + id("xyz.jpenilla.run-paper") version "3.0.2" +} + +version = "3.11.0" + +repositories { + mavenCentral() + maven { url = uri("https://repo.panda-lang.org/releases/") } + maven { url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") } +} + +dependencies { + compileOnly("org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT") + + // implementation("dev.rollczi:litecommands-bukkit:3.11.0") // <-- uncomment in your project + // implementation("dev.rollczi:litecommands-jakarta:3.11.0") // <-- uncomment in your project + implementation(project(":litecommands-bukkit")) // don't use this line in your build.gradle + implementation(project(":litecommands-jakarta")) // don't use this line in your build.gradle +} + +val pluginName = "ExampleBukkitJakartaPlugin" +val packageName = "dev.rollczi.example.bukkit.jakarta" + +bukkit { + main = "$packageName.$pluginName" + apiVersion = "1.13" + author = "Rollczi" + name = pluginName + version = "${project.version}" +} + +tasks.shadowJar { + archiveFileName.set("$pluginName v${project.version}.jar") + + listOf( + "dev.rollczi.litecommands", + ).forEach { relocate(it, "$packageName.libs.$it") } +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} + +tasks.withType { + options.compilerArgs.add("-parameters") + options.release = 21 +} + +sourceSets.test { + java.setSrcDirs(emptyList()) + resources.setSrcDirs(emptyList()) +} + +tasks.runServer { + minecraftVersion("1.21.4") +} diff --git a/examples/bukkit-jakarta/src/main/java/dev/rollczi/example/bukkit/jakarta/ExampleBukkitJakartaPlugin.java b/examples/bukkit-jakarta/src/main/java/dev/rollczi/example/bukkit/jakarta/ExampleBukkitJakartaPlugin.java new file mode 100644 index 000000000..b2f47ed2d --- /dev/null +++ b/examples/bukkit-jakarta/src/main/java/dev/rollczi/example/bukkit/jakarta/ExampleBukkitJakartaPlugin.java @@ -0,0 +1,32 @@ +package dev.rollczi.example.bukkit.jakarta; + +import dev.rollczi.example.bukkit.jakarta.command.JakartaCommand; +import dev.rollczi.litecommands.LiteCommands; +import dev.rollczi.litecommands.bukkit.LiteBukkitFactory; +import dev.rollczi.litecommands.jakarta.LiteJakartaExtension; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; + +public class ExampleBukkitJakartaPlugin extends JavaPlugin { + + private LiteCommands liteCommands; + + @Override + public void onEnable() { + this.liteCommands = LiteBukkitFactory.builder("my-jakarta-plugin") + .extension(new LiteJakartaExtension<>()) + .argument(JakartaCommand.User.class, new JakartaCommand.UserArgumentResolver()) + .commands( + new JakartaCommand() + ) + .build(); + } + + @Override + public void onDisable() { + if (this.liteCommands != null) { + this.liteCommands.unregister(); + } + } + +} diff --git a/examples/bukkit-jakarta/src/main/java/dev/rollczi/example/bukkit/jakarta/command/JakartaCommand.java b/examples/bukkit-jakarta/src/main/java/dev/rollczi/example/bukkit/jakarta/command/JakartaCommand.java new file mode 100644 index 000000000..1b16aa1a4 --- /dev/null +++ b/examples/bukkit-jakarta/src/main/java/dev/rollczi/example/bukkit/jakarta/command/JakartaCommand.java @@ -0,0 +1,206 @@ +package dev.rollczi.example.bukkit.jakarta.command; + +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.argument.parser.ParseResult; +import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.suggestion.SuggestionContext; +import dev.rollczi.litecommands.suggestion.SuggestionResult; +import org.bukkit.command.CommandSender; +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Negative; +import jakarta.validation.constraints.NegativeOrZero; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Instant; +import java.util.List; + +@Command(name = "jakarta") +public class JakartaCommand { + + @Execute(name = "age") + String age(@Min(1) @Max(120) @Arg int age) { + return "Your age is " + age; + } + + @Execute(name = "name") + String name(@NotBlank @Size(min = 3, max = 16) @Arg String name) { + return "Your name is " + name; + } + + @Execute(name = "boolean-true") + String booleanTrue(@AssertTrue @Arg boolean value) { + return "Value is " + value; + } + + @Execute(name = "boolean-false") + String booleanFalse(@AssertFalse @Arg boolean value) { + return "Value is " + value; + } + + @Execute(name = "positive") + String positive(@Positive @Arg int value) { + return "Positive value: " + value; + } + + @Execute(name = "positive-or-zero") + String positiveOrZero(@PositiveOrZero @Arg int value) { + return "Positive or zero value: " + value; + } + + @Execute(name = "negative") + String negative(@Negative @Arg int value) { + return "Negative value: " + value; + } + + @Execute(name = "negative-or-zero") + String negativeOrZero(@NegativeOrZero @Arg int value) { + return "Negative or zero value: " + value; + } + + @Execute(name = "digits") + String digits(@Digits(integer = 3, fraction = 2) @Arg double value) { + return "Digits: " + value; + } + + @Execute(name = "decimal") + String decimal(@DecimalMin("0.1") @DecimalMax("1.0") @Arg double value) { + return "Decimal value: " + value; + } + + @Execute(name = "past") + String past(@Past @Arg Instant instant) { + return "Past instant: " + instant; + } + + @Execute(name = "past-or-present") + String pastOrPresent(@PastOrPresent @Arg Instant instant) { + return "Past or present instant: " + instant; + } + + @Execute(name = "future") + String future(@Future @Arg Instant instant) { + return "Future instant: " + instant; + } + + @Execute(name = "future-or-present") + String futureOrPresent(@FutureOrPresent @Arg Instant instant) { + return "Future or present instant: " + instant; + } + + @Execute(name = "pattern") + String pattern(@Pattern(regexp = "[a-z]+") @Arg String text) { + return "Pattern match: " + text; + } + + @Execute(name = "email") + String email(@Email @Arg String email) { + return "Email: " + email; + } + + @Execute(name = "list") + String list(@NotEmpty @Size(min = 2) @Arg List list) { + return "List size: " + list.size(); + } + + @Execute(name = "user-info") + String userInfo(@NotBlank @Size(min = 3) @Arg String name, @Min(1) @Max(100) @Arg int level) { + return "User: " + name + ", Level: " + level; + } + + @Execute(name = "transfer") + String transfer(@NotBlank @Arg String from, @NotBlank @Arg String to, @Positive @Arg double amount) { + return "Transferred " + amount + " from " + from + " to " + to; + } + + @Execute(name = "user") + String user(@Valid @Arg User user) { + return "User: " + user.name + " (" + user.age + ")"; + } + + public record User(@NotBlank String name, @Min(18) int age) { + + public User(String name, int age) { + this.name = name; + this.age = age; + } + } + + public static class UserArgumentResolver extends ArgumentResolver { + + @Override + protected ParseResult parse(Invocation invocation, Argument context, String argument) { + String[] split = argument.split(":", 2); + + if (split.length < 2) { + return ParseResult.failure("Invalid user format. Use name:age"); + } + + try { + String name = split[0]; + int age = Integer.parseInt(split[1]); + + return ParseResult.success(new User(name, age)); + } catch (NumberFormatException e) { + return ParseResult.failure("Age must be a number."); + } + } + + @Override + public SuggestionResult suggest(Invocation invocation, Argument argument, SuggestionContext context) { + return SuggestionResult.of("name:18"); + } + + } + + @Execute(name = "custom") + private String custom(@NotAdmin @Arg String name) { + return "Hello, " + name; + } + + @Target({ ElementType.PARAMETER, ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = NotAdminValidator.class) + @Documented + private @interface NotAdmin { + String message() default "Name cannot be 'admin'"; + Class[] groups() default {}; + Class[] payload() default {}; + } + + private static class NotAdminValidator implements ConstraintValidator { + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || !value.equalsIgnoreCase("admin"); + } + } + +} diff --git a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java index 2663e17f8..47a567c5e 100644 --- a/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java +++ b/litecommands-annotations/src/dev/rollczi/litecommands/annotations/MethodCommandExecutor.java @@ -46,6 +46,7 @@ public class MethodCommandExecutor extends AbstractCommandExecutor extends Scopeable, CommandNode, CommandExecutorMatchResult match(RequirementsResult result); + default Object getInstance() { + return null; + } + @Unmodifiable @Override default Collection names() { diff --git a/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionResult.java b/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionResult.java index 6c98fe26e..23d58a169 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionResult.java +++ b/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionResult.java @@ -10,6 +10,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; public class SuggestionResult { @@ -38,6 +39,10 @@ public void clear() { this.suggestions.clear(); } + public void removeIf(Predicate predicate) { + this.suggestions.removeIf(predicate); + } + public void addAll(SuggestionResult result) { this.suggestions.addAll(result.suggestions); } diff --git a/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java b/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java index d32c4504b..e90a14061 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java +++ b/litecommands-core/src/dev/rollczi/litecommands/suggestion/SuggestionService.java @@ -2,18 +2,18 @@ import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.Parser; +import dev.rollczi.litecommands.argument.parser.ParserRegistry; import dev.rollczi.litecommands.argument.suggester.Suggester; import dev.rollczi.litecommands.argument.suggester.SuggesterRegistry; import dev.rollczi.litecommands.argument.suggester.input.SuggestionInputMatcher; import dev.rollczi.litecommands.argument.suggester.input.SuggestionInputResult; -import dev.rollczi.litecommands.argument.parser.ParserRegistry; -import dev.rollczi.litecommands.command.executor.CommandExecutor; import dev.rollczi.litecommands.command.CommandRoute; +import dev.rollczi.litecommands.command.executor.CommandExecutor; import dev.rollczi.litecommands.event.EventPublisher; -import dev.rollczi.litecommands.flow.Flow; import dev.rollczi.litecommands.invocation.Invocation; import dev.rollczi.litecommands.suggestion.event.SuggestionCommandRouteEvent; import dev.rollczi.litecommands.suggestion.event.SuggestionExecutorEvent; +import dev.rollczi.litecommands.suggestion.event.SuggestionResultEvent; import dev.rollczi.litecommands.util.StringUtil; import dev.rollczi.litecommands.validator.ValidatorService; @@ -123,7 +123,7 @@ public > SuggestionResult sugges SuggestionResult collector = SuggestionResult.empty(); for (Argument argument : executor.getArguments()) { - SuggestionInputResult result = suggestArgument(invocation, matcher, argument); + SuggestionInputResult result = suggestArgument(invocation, executor, matcher, argument); collector.addAll(result.getResult()); switch (result.getCause()) { @@ -138,6 +138,7 @@ public > SuggestionResult sugges private > SuggestionInputResult suggestArgument( Invocation invocation, + CommandExecutor executor, MATCHER matcher, Argument argument ) { @@ -146,6 +147,7 @@ private > SuggestionInpu Suggester suggester = suggesterRegistry.getSuggester(parsedType, argument.getKey()); SuggestionInputResult result = matcher.nextArgument(invocation, argument, parser, suggester); + this.publisher.publish(new SuggestionResultEvent(executor, argument, result.getResult())); if (result.isEnd() && matcher.isOptionalArgument(invocation, argument, parser)) { return SuggestionInputResult.continueWith(result); diff --git a/litecommands-core/src/dev/rollczi/litecommands/suggestion/event/SuggestionResultEvent.java b/litecommands-core/src/dev/rollczi/litecommands/suggestion/event/SuggestionResultEvent.java new file mode 100644 index 000000000..6f7e52196 --- /dev/null +++ b/litecommands-core/src/dev/rollczi/litecommands/suggestion/event/SuggestionResultEvent.java @@ -0,0 +1,14 @@ +package dev.rollczi.litecommands.suggestion.event; + +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.command.executor.CommandExecutor; +import dev.rollczi.litecommands.event.Event; +import dev.rollczi.litecommands.suggestion.SuggestionResult; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public record SuggestionResultEvent( + CommandExecutor executor, + Argument argument, + SuggestionResult result +) implements Event { } diff --git a/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/JakartaMethodValidator.java b/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/JakartaMethodValidator.java index 969a9c689..a675e6762 100644 --- a/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/JakartaMethodValidator.java +++ b/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/JakartaMethodValidator.java @@ -5,11 +5,10 @@ import dev.rollczi.litecommands.validator.ValidatorResult; import jakarta.validation.ConstraintViolation; import jakarta.validation.ElementKind; +import jakarta.validation.Path; import jakarta.validation.Validator; import jakarta.validation.executable.ExecutableValidator; -import jakarta.validation.Path; -import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -34,18 +33,18 @@ public ValidatorResult validate(MethodValidatorContext context) { List violationsList = violations.stream() .map(objectConstraintViolation -> { Path propertyPath = objectConstraintViolation.getPropertyPath(); - Path.Node leafNode = null; + Path.ParameterNode parameterNode = null; for (Path.Node node : propertyPath) { - leafNode = node; + if (node.getKind() == ElementKind.PARAMETER) { + parameterNode = node.as(Path.ParameterNode.class); + } } - if (leafNode == null || leafNode.getKind() != ElementKind.PARAMETER) { - throw new IllegalStateException("Invalid leaf node: " + (leafNode == null ? "null" : leafNode.getKind())); + if (parameterNode == null) { + throw new IllegalStateException("No parameter node found in path: " + propertyPath); } - Path.ParameterNode parameterNode = leafNode.as(Path.ParameterNode.class); - return new JakartaRawResult.Entry(objectConstraintViolation, context.getDefinition().getRequirement(parameterNode.getParameterIndex())); }).collect(Collectors.toList()); diff --git a/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/JakartaSuggestionListener.java b/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/JakartaSuggestionListener.java new file mode 100644 index 000000000..bd41d38dc --- /dev/null +++ b/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/JakartaSuggestionListener.java @@ -0,0 +1,192 @@ +package dev.rollczi.litecommands.jakarta; + +import dev.rollczi.litecommands.annotations.meta.MetaAnnotationKeys; +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.suggestion.Suggestion; +import dev.rollczi.litecommands.suggestion.event.SuggestionResultEvent; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ElementKind; +import jakarta.validation.Path; +import jakarta.validation.Validator; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Logger; + +class JakartaSuggestionListener implements Consumer { + + private static final Logger LOGGER = Logger.getLogger("LiteCommands"); + + private final Validator validator; + + JakartaSuggestionListener(Validator validator) { + this.validator = validator; + } + + @Override + public void accept(SuggestionResultEvent event) { + Argument argument = event.argument(); + Parameter parameter = argument.meta().get(MetaAnnotationKeys.SOURCE_PARAMETER); + + Executable executable = parameter.getDeclaringExecutable(); + if (!(executable instanceof Method method)) { + return; + } + + int parameterIndex = getParameterIndex(parameter); + if (parameterIndex == -1) { + return; + } + + Object handle = event.executor().getInstance(); + + event.result().removeIf(suggestion -> + !isValidSuggestion(suggestion, argument, method, handle, parameterIndex) + ); + } + + private boolean isValidSuggestion( + Suggestion suggestion, + Argument argument, + Method method, + Object handle, + int parameterIndex + ) { + Object parsedValue = Parser.parse( + suggestion.multilevel(), + argument.getType().getRawType() + ); + + if (parsedValue == null) { + return false; + } + + Object[] parameters = buildParameters(method, parameterIndex, parsedValue); + + try { + Set> violations = + validator.forExecutables().validateParameters(handle, method, parameters); + + return !hasViolationForParameter(violations, parameterIndex); + + } catch (IllegalArgumentException e) { + LOGGER.warning(e.getMessage()); + return false; + } + } + + private Object[] buildParameters(Method method, int index, Object value) { + Class[] types = method.getParameterTypes(); + Object[] params = new Object[types.length]; + + for (int i = 0; i < types.length; i++) { + params[i] = (i == index) ? value : Defaults.get(types[i]); + } + + return params; + } + + private boolean hasViolationForParameter( + Set> violations, + int parameterIndex + ) { + for (ConstraintViolation violation : violations) { + for (Path.Node node : violation.getPropertyPath()) { + + if (node.getKind() != ElementKind.PARAMETER) { + continue; + } + + if (((Path.ParameterNode) node).getParameterIndex() == parameterIndex) { + return true; + } + } + } + return false; + } + + private int getParameterIndex(Parameter parameter) { + Parameter[] parameters = parameter.getDeclaringExecutable().getParameters(); + for (int i = 0; i < parameters.length; i++) { + if (parameters[i].equals(parameter)) { + return i; + } + } + return -1; + } + + private static final class Parser { + + private static final Map, Function> PARSERS = Map.of( + Byte.class, Byte::parseByte, + Short.class, Short::parseShort, + Integer.class, Integer::parseInt, + Long.class, Long::parseLong, + Float.class, Float::parseFloat, + Double.class, Double::parseDouble, + Boolean.class, Boolean::parseBoolean, + Character.class, s -> s.isEmpty() ? null : s.charAt(0), + String.class, Function.identity() + ); + + @SuppressWarnings("unchecked") + @Nullable + private static T parse(String value, Class type) { + Function parser = PARSERS.get(normalize(type)); + + if (parser == null) { + return null; + } + + try { + return (T) parser.apply(value); + } catch (RuntimeException e) { + return null; + } + } + + private static Class normalize(Class type) { + if (!type.isPrimitive()) { + return type; + } + + // When java 21 comes, switch to type matching `(switch(type))` + return switch (type.getName()) { + case "byte" -> Byte.class; + case "short" -> Short.class; + case "int" -> Integer.class; + case "long" -> Long.class; + case "float" -> Float.class; + case "double" -> Double.class; + case "boolean" -> Boolean.class; + case "char" -> Character.class; + default -> type; + }; + } + } + + private static final class Defaults { + + private static final Map, Object> DEFAULTS = Map.of( + boolean.class, false, + char.class, '\0', + byte.class, (byte) 0, + short.class, (short) 0, + int.class, 0, + long.class, 0L, + float.class, 0f, + double.class, 0d + ); + + @Nullable + private static Object get(Class type) { + return DEFAULTS.get(type); + } + } +} diff --git a/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/LiteJakartaExtension.java b/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/LiteJakartaExtension.java index 813d7096a..76f4e7af9 100644 --- a/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/LiteJakartaExtension.java +++ b/litecommands-jakarta/src/dev/rollczi/litecommands/jakarta/LiteJakartaExtension.java @@ -4,6 +4,8 @@ import dev.rollczi.litecommands.LiteCommandsInternal; import dev.rollczi.litecommands.configurator.LiteConfigurator; import dev.rollczi.litecommands.extension.LiteExtension; +import dev.rollczi.litecommands.suggestion.event.SuggestionResultEvent; +import jakarta.validation.Validator; public class LiteJakartaExtension implements LiteExtension> { @@ -16,10 +18,13 @@ public void configure(LiteConfigurator> configurer) { @Override public void extend(LiteCommandsBuilder builder, LiteCommandsInternal internal) { + Validator validator = settings.getOrCreateValidatorFactory().getValidator(); + builder .result(JakartaRawResult.class, new JakartaRawResultHandler<>(settings, internal.getSchematicGenerator())) .annotations(configuration -> configuration - .methodValidator(new JakartaMethodValidator<>(settings.getOrCreateValidatorFactory().getValidator()))); + .methodValidator(new JakartaMethodValidator<>(validator))) + .listener(SuggestionResultEvent.class, new JakartaSuggestionListener(validator)); } } diff --git a/litecommands-jakarta/test/dev/rollczi/litecommands/jakarta/JakartaSuggestionTest.java b/litecommands-jakarta/test/dev/rollczi/litecommands/jakarta/JakartaSuggestionTest.java new file mode 100644 index 000000000..7af01c1e1 --- /dev/null +++ b/litecommands-jakarta/test/dev/rollczi/litecommands/jakarta/JakartaSuggestionTest.java @@ -0,0 +1,55 @@ +package dev.rollczi.litecommands.jakarta; + +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.unit.LiteCommandsTestFactory; +import dev.rollczi.litecommands.unit.TestPlatform; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import org.junit.jupiter.api.Test; + +class JakartaSuggestionTest { + + @Command(name = "test") + static class TestCommand { + @Execute(name = "min") + public void min(@Min(1) @Arg int number) {} + + @Execute(name = "max") + public void max(@Max(10) @Arg int number) {} + } + + @Command(name = "unrelated") + static class UnrelatedTestCommand { + @Execute(name = "test") + public void test(@Min(5) @Arg int other, @Min(5) @Max(10) @Arg int target) {} + } + + TestPlatform platform = LiteCommandsTestFactory.startPlatform(config -> config + .extension(new LiteJakartaExtension<>()) + .commands(new TestCommand(), new UnrelatedTestCommand()) + ); + + @Test + void testMinSuggestion() { + platform.suggest("test min ") + .assertNotEmpty() + .assertNotSuggest("0"); + } + + @Test + void testMaxSuggestion() { + platform.suggest("test max ") + .assertNotEmpty() + .assertNotSuggest("11", "50", "100", "500"); + } + + @Test + void testTargetSuggestionWithUnrelatedMinConstraint() { + platform.suggest("unrelated test 0 ") + .assertSuggestAndFlush("5", "6", "7", "8", "9", "10") + .assertNotSuggest("4", "11"); + } + +} diff --git a/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertSuggest.java b/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertSuggest.java index 9011a1b7b..9f447b96f 100644 --- a/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertSuggest.java +++ b/litecommands-unit/src/dev/rollczi/litecommands/unit/AssertSuggest.java @@ -151,4 +151,11 @@ public AssertSuggest assertEmpty() { return this; } + public AssertSuggest assertNotSuggest(String... suggestions) { + assertThat(suggest.getSuggestions()) + .map(Suggestion::multilevel) + .doesNotContain(suggestions); + + return this; + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index b8815a5d6..ed8d31c8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,7 @@ include(":litecommands-telegrambots", tests = false) include(":examples:bukkit", tests = false) include(":examples:bukkit-adventure-platform", tests = false) include(":examples:bukkit-chatgpt", tests = false) +include(":examples:bukkit-jakarta", tests = false) include(":examples:minestom", toVersion("25"), tests = false) include(":examples:paper", VERSION_21, tests = false) include(":examples:fabric", tests = false)