Skip to content

Commit 711edf9

Browse files
authored
GH-263 Add API for requirements validators in command processing pipeline. (#264)
* Add API for requirements validators in a command processing pipeline. * Add FlyCommand and IsNotOp validation, for example.
1 parent 909e55e commit 711edf9

File tree

17 files changed

+380
-109
lines changed

17 files changed

+380
-109
lines changed

examples/bukkit/src/main/java/dev/rollczi/example/bukkit/ExamplePlugin.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
import dev.rollczi.example.bukkit.argument.GameModeArgument;
44
import dev.rollczi.example.bukkit.command.ConvertCommand;
5+
import dev.rollczi.example.bukkit.command.FlyCommand;
56
import dev.rollczi.example.bukkit.command.GameModeCommand;
67
import dev.rollczi.example.bukkit.command.KickCommand;
78
import dev.rollczi.example.bukkit.command.TeleportCommand;
9+
import dev.rollczi.example.bukkit.validator.IsNotOpValidator;
10+
import dev.rollczi.example.bukkit.validator.IsNotOp;
811
import dev.rollczi.litecommands.bukkit.LiteBukkitMessages;
912
import dev.rollczi.example.bukkit.handler.ExampleInvalidUsageHandler;
1013
import dev.rollczi.example.bukkit.handler.ExampleMissingPermissionsHandler;
1114
import dev.rollczi.litecommands.LiteCommands;
1215
import dev.rollczi.litecommands.annotations.LiteCommandsAnnotations;
16+
import dev.rollczi.litecommands.extension.annotations.LiteAnnotationsProcessorExtension;
1317
import dev.rollczi.litecommands.join.JoinArgument;
1418
import dev.rollczi.litecommands.programmatic.LiteCommand;
1519
import dev.rollczi.litecommands.programmatic.LiteCommandsProgrammatic;
@@ -39,8 +43,16 @@ public void onEnable() {
3943
new ConvertCommand(),
4044
new GameModeCommand(),
4145
new KickCommand(),
42-
new TeleportCommand()
46+
new TeleportCommand(),
47+
new FlyCommand()
4348
))
49+
50+
// Custom annotation validators
51+
.extension(new LiteAnnotationsProcessorExtension<>(), extension -> extension
52+
.validator(Player.class, IsNotOp.class, new IsNotOpValidator()) // see FlyCommand
53+
)
54+
55+
// Programmatic commands
4456
.commands(LiteCommandsProgrammatic.of(
4557
new LiteCommand<CommandSender>("ban")
4658
.permissions("example.ban")
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.rollczi.example.bukkit.command;
2+
3+
import dev.rollczi.example.bukkit.validator.IsNotOp;
4+
import dev.rollczi.litecommands.annotations.argument.Arg;
5+
import dev.rollczi.litecommands.annotations.command.Command;
6+
import dev.rollczi.litecommands.annotations.context.Context;
7+
import dev.rollczi.litecommands.annotations.execute.Execute;
8+
import dev.rollczi.litecommands.annotations.permission.Permission;
9+
import org.bukkit.command.CommandSender;
10+
import org.bukkit.entity.Player;
11+
12+
@Command(name = "fly", aliases = {"f"})
13+
@Permission("example.fly")
14+
public class FlyCommand {
15+
16+
@Execute
17+
void execute(@Context Player sender) {
18+
sender.setAllowFlight(!sender.getAllowFlight());
19+
sender.sendMessage("You can " + (sender.getAllowFlight() ? "now" : "no longer") + " fly!");
20+
}
21+
22+
@Execute
23+
void executeOther(@Context CommandSender sender, @Arg @IsNotOp Player target) {
24+
target.setAllowFlight(!target.getAllowFlight());
25+
target.sendMessage("You can " + (target.getAllowFlight() ? "now" : "no longer") + " fly!");
26+
sender.sendMessage("You " + (target.getAllowFlight() ? "enabled" : "disabled") + " fly for " + target.getName());
27+
}
28+
29+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.rollczi.example.bukkit.validator;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target(ElementType.PARAMETER)
10+
public @interface IsNotOp {
11+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.rollczi.example.bukkit.validator;
2+
3+
import dev.rollczi.litecommands.annotations.validator.requirment.AnnotatedValidator;
4+
import dev.rollczi.litecommands.command.executor.CommandExecutor;
5+
import dev.rollczi.litecommands.invocation.Invocation;
6+
import dev.rollczi.litecommands.requirement.Requirement;
7+
import dev.rollczi.litecommands.validator.ValidatorResult;
8+
import org.bukkit.command.CommandSender;
9+
import org.bukkit.entity.Player;
10+
11+
public class IsNotOpValidator implements AnnotatedValidator<CommandSender, Player, IsNotOp> {
12+
13+
@Override
14+
public ValidatorResult validate(
15+
Invocation<CommandSender> invocation,
16+
CommandExecutor<CommandSender> executor,
17+
Requirement<Player> requirement,
18+
Player player,
19+
IsNotOp annotation
20+
) {
21+
if (player.isOp()) {
22+
return ValidatorResult.invalid("Player " + player.getName() + " is server operator!");
23+
}
24+
25+
return ValidatorResult.valid();
26+
}
27+
28+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dev.rollczi.litecommands.annotations.validator.requirment;
2+
3+
import dev.rollczi.litecommands.command.executor.CommandExecutor;
4+
import dev.rollczi.litecommands.invocation.Invocation;
5+
import dev.rollczi.litecommands.requirement.Requirement;
6+
import dev.rollczi.litecommands.validator.ValidatorResult;
7+
8+
import java.lang.annotation.Annotation;
9+
10+
@FunctionalInterface
11+
public interface AnnotatedValidator<SENDER, T, A extends Annotation> {
12+
13+
ValidatorResult validate(Invocation<SENDER> invocation, CommandExecutor<SENDER> executor, Requirement<T> requirement, T value, A annotation);
14+
15+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package dev.rollczi.litecommands.annotations.validator.requirment;
2+
3+
import dev.rollczi.litecommands.annotations.AnnotationInvoker;
4+
import dev.rollczi.litecommands.annotations.AnnotationProcessor;
5+
import dev.rollczi.litecommands.meta.Meta;
6+
7+
import java.lang.annotation.Annotation;
8+
9+
public class AnnotatedValidatorProcessor<SENDER, T, A extends Annotation> implements AnnotationProcessor<SENDER> {
10+
11+
private final Class<A> annotationClass;
12+
private final Class<T> type;
13+
private final AnnotatedValidator<SENDER, T, A> annotatedValidator;
14+
15+
public AnnotatedValidatorProcessor(Class<A> annotationClass, Class<T> type, AnnotatedValidator<SENDER, T, A> annotatedValidator) {
16+
this.annotationClass = annotationClass;
17+
this.type = type;
18+
this.annotatedValidator = annotatedValidator;
19+
}
20+
21+
@Override
22+
public AnnotationInvoker<SENDER> process(AnnotationInvoker<SENDER> invoker) {
23+
return invoker.onRequirementMeta(annotationClass, (annotationHolder, builder, requirement) -> {
24+
Class<?> parsedType = requirement.getWrapperFormat().getParsedType();
25+
26+
if (!type.isAssignableFrom(parsedType)) {
27+
return;
28+
}
29+
30+
RequirementAnnotatedValidatorImpl<SENDER, T, A> validator = new RequirementAnnotatedValidatorImpl<>(annotatedValidator, annotationHolder.getAnnotation());
31+
32+
requirement.meta().listEditor(Meta.REQUIREMENT_VALIDATORS)
33+
.add(validator)
34+
.apply();
35+
});
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package dev.rollczi.litecommands.annotations.validator.requirment;
2+
3+
import dev.rollczi.litecommands.command.executor.CommandExecutor;
4+
import dev.rollczi.litecommands.invocation.Invocation;
5+
import dev.rollczi.litecommands.requirement.Requirement;
6+
import dev.rollczi.litecommands.validator.ValidatorResult;
7+
import dev.rollczi.litecommands.validator.requirment.RequirementValidator;
8+
9+
import java.lang.annotation.Annotation;
10+
11+
class RequirementAnnotatedValidatorImpl<SENDER, T, A extends Annotation> implements RequirementValidator<SENDER, T> {
12+
13+
private final AnnotatedValidator<SENDER, T, A> validator;
14+
private final A annotation;
15+
16+
public RequirementAnnotatedValidatorImpl(AnnotatedValidator<SENDER, T, A> validator, A annotation) {
17+
this.validator = validator;
18+
this.annotation = annotation;
19+
}
20+
21+
public ValidatorResult validate(Invocation<SENDER> invocation, CommandExecutor<SENDER> executor, Requirement<T> requirement, T value) {
22+
return validator.validate(invocation, executor, requirement, value, annotation);
23+
}
24+
25+
}

litecommands-annotations/test/dev/rollczi/litecommands/annotations/LiteTestSpec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public static void beforeAll(TestInfo testInfo) {
3737
platform = new TestPlatform();
3838

3939
liteCommands = LiteCommandsFactory.builder(TestSender.class, platform)
40-
.commands(LiteCommandsAnnotations.ofClasses(commands))
4140
.preProcessor((builder, internal) -> configureLiteTest(builder, type))
41+
.commands(LiteCommandsAnnotations.ofClasses(commands))
4242
.exceptionUnexpected((invocation, exception, chain) -> {})
4343
.build(true);
4444
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package dev.rollczi.litecommands.annotations.validator.requirment;
2+
3+
import dev.rollczi.litecommands.annotations.LiteConfig;
4+
import dev.rollczi.litecommands.annotations.LiteTestSpec;
5+
import dev.rollczi.litecommands.annotations.argument.Arg;
6+
import dev.rollczi.litecommands.annotations.command.Command;
7+
import dev.rollczi.litecommands.annotations.execute.Execute;
8+
import dev.rollczi.litecommands.command.executor.CommandExecutor;
9+
import dev.rollczi.litecommands.extension.annotations.LiteAnnotationsProcessorExtension;
10+
import dev.rollczi.litecommands.invocation.Invocation;
11+
import dev.rollczi.litecommands.requirement.Requirement;
12+
import dev.rollczi.litecommands.unit.TestSender;
13+
import dev.rollczi.litecommands.validator.ValidatorResult;
14+
import org.junit.jupiter.api.Test;
15+
16+
import java.lang.annotation.Retention;
17+
import java.lang.annotation.RetentionPolicy;
18+
19+
class AnnotatedValidatorTest extends LiteTestSpec {
20+
21+
static LiteConfig config = builder -> builder
22+
.extension(new LiteAnnotationsProcessorExtension<>(), extension -> extension
23+
.validator(String.class, IsStupid.class, new IsStupidValidator())
24+
);
25+
26+
@Retention(RetentionPolicy.RUNTIME)
27+
@interface IsStupid {
28+
}
29+
30+
static class IsStupidValidator implements AnnotatedValidator<TestSender, String, IsStupid> {
31+
32+
@Override
33+
public ValidatorResult validate(Invocation<TestSender> invocation, CommandExecutor<TestSender> executor, Requirement<String> requirement, String value, IsStupid annotation) {
34+
if (value.equals("stupid")) {
35+
return ValidatorResult.invalid("You are stupid!");
36+
}
37+
38+
return ValidatorResult.valid();
39+
}
40+
}
41+
42+
@Command(name = "command")
43+
static class TestCommand {
44+
@Execute
45+
public String execute(@Arg @IsStupid String arg) {
46+
return arg;
47+
}
48+
}
49+
50+
@Test
51+
void testFailureValidation() {
52+
platform.execute("command stupid")
53+
.assertFailure("You are stupid!");
54+
}
55+
56+
@Test
57+
void testSuccessValidation() {
58+
platform.execute("command not-stupid")
59+
.assertSuccess("not-stupid");
60+
}
61+
62+
}

litecommands-core/src/dev/rollczi/litecommands/argument/parser/ParseResult.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package dev.rollczi.litecommands.argument.parser;
22

3+
import dev.rollczi.litecommands.requirement.RequirementResult;
34
import dev.rollczi.litecommands.shared.FailedReason;
5+
import org.jetbrains.annotations.NotNull;
46
import org.jetbrains.annotations.Nullable;
57

68
import java.util.Objects;
79

8-
public class ParseResult<EXPECTED> {
10+
public class ParseResult<EXPECTED> implements RequirementResult<EXPECTED> {
911

1012
private final @Nullable EXPECTED successfulResult;
1113
private final @Nullable FailedReason failedResult;
@@ -23,28 +25,32 @@ private ParseResult(@Nullable EXPECTED successfulResult, @Nullable FailedReason
2325
this.failedResult = failedResult;
2426
}
2527

28+
@Override
2629
public boolean isSuccessful() {
2730
return this.successfulResult != null;
2831
}
2932

33+
@Override
3034
public boolean isFailed() {
3135
return this.failedResult != null;
3236
}
3337

34-
public EXPECTED getSuccessfulResult() {
38+
@Override
39+
public @NotNull EXPECTED getSuccess() {
3540
if (this.successfulResult == null) {
3641
throw new IllegalStateException("Cannot get successful result when it is empty");
3742
}
3843

3944
return this.successfulResult;
4045
}
4146

42-
public FailedReason getFailedReason() {
47+
@Override
48+
public @NotNull Object getFailedReason() {
4349
if (this.failedResult == null) {
4450
throw new IllegalStateException("Cannot get failed reason when it is empty");
4551
}
4652

47-
return this.failedResult;
53+
return this.failedResult.getReason();
4854
}
4955

5056
public static <PARSED> ParseResult<PARSED> success(PARSED parsed) {

0 commit comments

Comments
 (0)