feat: annotation string processors (#353)

adds a system for processing strings found in annotations before they're used by AnnotationParser

implements #347

Also, because we're using "-Werror", the code won't actually build (and thus tests won't work) using JDK18. To remedy this, a bunch of @SuppressWarnings("serial")s has been added to the exceptions. We don't serialize exceptions, and they're in fact non-serializable because of their members, so this is the appropriate solution (well, the better solution would be to make them serializable, but that's outside the scope of this PR).
This commit is contained in:
Alexander Söderberg 2022-05-26 06:53:54 +02:00 committed by Jason
parent ed7b7569a8
commit d681ba5840
28 changed files with 715 additions and 38 deletions

View file

@ -0,0 +1,49 @@
//
// MIT License
//
// Copyright (c) 2021 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.annotations;
import static com.google.common.truth.Truth.assertThat;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
class NoOpStringProcessorTest {
@Test
void ProcessString_AnyInput_ReturnsOriginalInput() {
// Will force the input string to be scrambled 10 times.
for (int i = 0; i < 10; i++) {
// Arrange
final StringProcessor stringProcessor = StringProcessor.noOp();
final String input = ThreadLocalRandom.current().ints().mapToObj(Integer::toString).limit(32).collect(Collectors.joining());
// Act
final String output = stringProcessor.processString(input);
// Assert
assertThat(input).isEqualTo(output);
}
}
}

View file

@ -0,0 +1,92 @@
//
// MIT License
//
// Copyright (c) 2021 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.annotations;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class PatternReplacingStringProcessorTest {
private static final Pattern TEST_PATTERN = Pattern.compile("\\[(\\S+)]");
private PatternReplacingStringProcessor patternReplacingStringProcessor;
@Mock
private Function<MatchResult, String> replacementProvider;
@BeforeEach
void setup() {
this.patternReplacingStringProcessor = new PatternReplacingStringProcessor(
TEST_PATTERN,
this.replacementProvider
);
}
@Test
void ProcessString_MatchingInput_ReplacesGroups() {
// Arrange
when(this.replacementProvider.apply(any())).thenAnswer(iom -> iom.getArgument(0, MatchResult.class).group(1));
final String input = "[hello] [world]!";
// Act
final String output = this.patternReplacingStringProcessor.processString(input);
// Act
assertThat(output).isEqualTo("hello world!");
verify(this.replacementProvider, times(2)).apply(notNull());
verifyNoMoreInteractions(this.replacementProvider);
}
@Test
void ProcessString_NullReplacement_InputPreserved() {
// Arrange
final String input = "[input] ...";
// Act
final String output = this.patternReplacingStringProcessor.processString(input);
// Act
assertThat(output).isEqualTo(input);
verify(this.replacementProvider).apply(notNull());
verifyNoMoreInteractions(this.replacementProvider);
}
}

View file

@ -0,0 +1,102 @@
//
// MIT License
//
// Copyright (c) 2021 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.annotations;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class PropertyReplacingStringProcessorTest {
private PropertyReplacingStringProcessor propertyReplacingStringProcessor;
@Mock
private Function<String, String> propertyProvider;
@BeforeEach
void setup() {
this.propertyReplacingStringProcessor = new PropertyReplacingStringProcessor(this.propertyProvider);
}
@Test
void ProcessString_KnownProperty_ReplacesWithValue() {
// Arrange
when(this.propertyProvider.apply(anyString())).thenAnswer(iom -> "transformed: " + iom.getArgument(0, String.class));
final String input = "${hello.world}";
// Act
final String output = this.propertyReplacingStringProcessor.processString(input);
// Assert
assertThat(output).isEqualTo("transformed: hello.world");
verify(this.propertyProvider).apply("hello.world");
verifyNoMoreInteractions(this.propertyProvider);
}
@Test
void ProcessString_MultipleProperties_ReplacesAll() {
// Arrange
when(this.propertyProvider.apply(anyString())).thenAnswer(iom -> iom.getArgument(0, String.class));
final String input = "${cats} are cute, and so are ${dogs}!";
// Act
final String output = this.propertyReplacingStringProcessor.processString(input);
// Assert
assertThat(output).isEqualTo("cats are cute, and so are dogs!");
verify(this.propertyProvider).apply("cats");
verify(this.propertyProvider).apply("dogs");
verifyNoMoreInteractions(this.propertyProvider);
}
@Test
void ProcessString_NullProperty_InputPreserved() {
// Arrange
final String input = "${input} ...";
// Act
final String output = this.propertyReplacingStringProcessor.processString(input);
// Act
assertThat(output).isEqualTo(input);
verify(this.propertyProvider).apply(notNull());
verifyNoMoreInteractions(this.propertyProvider);
}
}

View file

@ -30,7 +30,7 @@ import cloud.commandframework.meta.SimpleCommandMeta;
public class TestCommandManager extends CommandManager<TestCommandSender> {
protected TestCommandManager() {
public TestCommandManager() {
super(CommandExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler());
}

View file

@ -0,0 +1,133 @@
//
// MIT License
//
// Copyright (c) 2021 Alexander Söderberg & Contributors
//
// 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 cloud.commandframework.annotations.feature;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import cloud.commandframework.Command;
import cloud.commandframework.annotations.AnnotationParser;
import cloud.commandframework.annotations.Argument;
import cloud.commandframework.annotations.CommandDescription;
import cloud.commandframework.annotations.CommandMethod;
import cloud.commandframework.annotations.CommandPermission;
import cloud.commandframework.annotations.Flag;
import cloud.commandframework.annotations.PropertyReplacingStringProcessor;
import cloud.commandframework.annotations.TestCommandManager;
import cloud.commandframework.annotations.TestCommandSender;
import cloud.commandframework.arguments.CommandArgument;
import cloud.commandframework.arguments.compound.FlagArgument;
import cloud.commandframework.arguments.flags.CommandFlag;
import cloud.commandframework.arguments.parser.StandardParameters;
import cloud.commandframework.meta.CommandMeta;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class StringProcessingTest {
private AnnotationParser<TestCommandSender> annotationParser;
private TestCommandManager commandManager;
@BeforeEach
void setup() {
this.commandManager = new TestCommandManager();
this.annotationParser = new AnnotationParser<>(
this.commandManager,
TestCommandSender.class,
p -> CommandMeta.simple()
.with(CommandMeta.DESCRIPTION, p.get(StandardParameters.DESCRIPTION, "No description"))
.build()
);
}
@Test
@DisplayName("Tests @CommandMethod, @CommandPermission, @CommandDescription, @Argument & @Flag")
@SuppressWarnings("unchecked")
void testStringProcessing() {
// Arrange
final String testProperty = ThreadLocalRandom.current()
.ints()
.mapToObj(Integer::toString)
.limit(32)
.collect(Collectors.joining());
final String testFlagName = ThreadLocalRandom.current()
.ints()
.mapToObj(Integer::toString)
.limit(32)
.collect(Collectors.joining());
this.annotationParser.stringProcessor(
new PropertyReplacingStringProcessor(
s -> ImmutableMap.of(
"property.test", testProperty,
"property.arg", "argument",
"property.flag", testFlagName
).get(s)
)
);
final TestClassA testClassA = new TestClassA();
// Act
this.annotationParser.parse(testClassA);
// Assert
final List<Command<TestCommandSender>> commands = new ArrayList<>(this.commandManager.getCommands());
assertThat(commands).hasSize(1);
final Command<TestCommandSender> command = commands.get(0);
assertThat(command.toString()).isEqualTo(String.format("%s argument flags", testProperty));
assertThat(command.getCommandPermission().toString()).isEqualTo(testProperty);
assertThat(command.getCommandMeta().get(CommandMeta.DESCRIPTION)).hasValue(testProperty);
final List<CommandArgument<TestCommandSender, ?>> arguments = command.getArguments();
assertThat(arguments).hasSize(3);
final FlagArgument<TestCommandSender> flagArgument = (FlagArgument<TestCommandSender>) arguments.get(2);
assertThat(flagArgument).isNotNull();
final List<CommandFlag<?>> flags = new ArrayList<>(flagArgument.getFlags());
assertThat(flags).hasSize(1);
assertThat(flags.get(0).getName()).isEqualTo(testFlagName);
}
private static class TestClassA {
@CommandDescription("${property.test}")
@CommandPermission("${property.test}")
@CommandMethod("${property.test} <argument>")
public void commandA(
final TestCommandSender sender,
@Argument("${property.arg}") final String arg,
@Flag("${property.flag}") final String flag
) {
}
}
}