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

@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353))
### Fixed ### Fixed
- Fix missing caption registration for the regex caption ([#351](https://github.com/Incendo/cloud/pull/351)) - Fix missing caption registration for the regex caption ([#351](https://github.com/Incendo/cloud/pull/351))

View file

@ -114,6 +114,7 @@ dependencies {
testImplementation(libs.jupiterEngine) testImplementation(libs.jupiterEngine)
testImplementation(libs.mockitoCore) testImplementation(libs.mockitoCore)
testImplementation(libs.mockitoKotlin) testImplementation(libs.mockitoKotlin)
testImplementation(libs.mockitoJupiter)
testImplementation(libs.truth) testImplementation(libs.truth)
testImplementation(libs.truthJava8) testImplementation(libs.truthJava8)
errorprone(libs.errorproneCore) errorprone(libs.errorproneCore)

View file

@ -96,6 +96,8 @@ public final class AnnotationParser<C> {
private final MetaFactory metaFactory; private final MetaFactory metaFactory;
private final FlagExtractor flagExtractor; private final FlagExtractor flagExtractor;
private StringProcessor stringProcessor;
/** /**
* Construct a new annotation parser * Construct a new annotation parser
* *
@ -118,12 +120,12 @@ public final class AnnotationParser<C> {
this.preprocessorMappers = new HashMap<>(); this.preprocessorMappers = new HashMap<>();
this.builderModifiers = new HashMap<>(); this.builderModifiers = new HashMap<>();
this.commandMethodFactories = new HashMap<>(); this.commandMethodFactories = new HashMap<>();
this.flagExtractor = new FlagExtractor(manager); this.flagExtractor = new FlagExtractor(manager, this);
this.registerAnnotationMapper(CommandDescription.class, d -> this.registerAnnotationMapper(CommandDescription.class, d ->
ParserParameters.single(StandardParameters.DESCRIPTION, d.value())); ParserParameters.single(StandardParameters.DESCRIPTION, this.processString(d.value())));
this.registerPreprocessorMapper(Regex.class, annotation -> RegexPreprocessor.of( this.registerPreprocessorMapper(Regex.class, annotation -> RegexPreprocessor.of(
annotation.value(), this.processString(annotation.value()),
Caption.of(annotation.failureCaption()) Caption.of(this.processString(annotation.failureCaption()))
)); ));
this.getParameterInjectorRegistry().registerInjector( this.getParameterInjectorRegistry().registerInjector(
String[].class, String[].class,
@ -131,6 +133,7 @@ public final class AnnotationParser<C> {
? null ? null
: context.getRawInput().toArray(new String[0]) : context.getRawInput().toArray(new String[0])
); );
this.stringProcessor = StringProcessor.noOp();
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -273,6 +276,48 @@ public final class AnnotationParser<C> {
return this.manager.parameterInjectorRegistry(); return this.manager.parameterInjectorRegistry();
} }
/**
* Returns the string processor used by this parser.
*
* @return the string processor
* @since 1.7.0
*/
public @NonNull StringProcessor stringProcessor() {
return this.stringProcessor;
}
/**
* Replaces the string processor of this parser.
*
* @param stringProcessor the new string processor
* @since 1.7.0
*/
public void stringProcessor(final @NonNull StringProcessor stringProcessor) {
this.stringProcessor = stringProcessor;
}
/**
* Processes the {@code input} string and returns the processed result.
*
* @param input the input string
* @return the processed string
* @since 1.7.0
*/
public @NonNull String processString(final @NonNull String input) {
return this.stringProcessor().processString(input);
}
/**
* Processes the input {@code strings} and returns the processed result.
*
* @param strings the input strings
* @return the processed strings
* @since 1.7.0
*/
public @NonNull String[] processStrings(final @NonNull String[] strings) {
return Arrays.stream(strings).map(this::processString).toArray(String[]::new);
}
/** /**
* Scan a class instance of {@link CommandMethod} annotations and attempt to * Scan a class instance of {@link CommandMethod} annotations and attempt to
* compile them into {@link Command} instances * compile them into {@link Command} instances
@ -336,7 +381,7 @@ public final class AnnotationParser<C> {
} }
try { try {
this.manager.getParserRegistry().registerSuggestionProvider( this.manager.getParserRegistry().registerSuggestionProvider(
suggestions.value(), this.processString(suggestions.value()),
new MethodSuggestionsProvider<>(instance, method) new MethodSuggestionsProvider<>(instance, method)
); );
} catch (final Exception e) { } catch (final Exception e) {
@ -367,15 +412,16 @@ public final class AnnotationParser<C> {
)); ));
} }
try { try {
final String suggestions = this.processString(parser.suggestions());
final BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider; final BiFunction<CommandContext<C>, String, List<String>> suggestionsProvider;
if (parser.suggestions().isEmpty()) { if (suggestions.isEmpty()) {
suggestionsProvider = (context, input) -> Collections.emptyList(); suggestionsProvider = (context, input) -> Collections.emptyList();
} else { } else {
suggestionsProvider = this.manager.getParserRegistry().getSuggestionProvider(parser.suggestions()) suggestionsProvider = this.manager.getParserRegistry().getSuggestionProvider(suggestions)
.orElseThrow(() -> new NullPointerException( .orElseThrow(() -> new NullPointerException(
String.format( String.format(
"Cannot find the suggestions provider with name '%s'", "Cannot find the suggestions provider with name '%s'",
parser.suggestions() suggestions
) )
)); ));
} }
@ -386,14 +432,15 @@ public final class AnnotationParser<C> {
); );
final Function<ParserParameters, ArgumentParser<C, ?>> parserFunction = final Function<ParserParameters, ArgumentParser<C, ?>> parserFunction =
parameters -> methodArgumentParser; parameters -> methodArgumentParser;
if (parser.name().isEmpty()) { final String name = this.processString(parser.name());
if (name.isEmpty()) {
this.manager.getParserRegistry().registerParserSupplier( this.manager.getParserRegistry().registerParserSupplier(
TypeToken.get(method.getGenericReturnType()), TypeToken.get(method.getGenericReturnType()),
parserFunction parserFunction
); );
} else { } else {
this.manager.getParserRegistry().registerNamedParserSupplier( this.manager.getParserRegistry().registerNamedParserSupplier(
parser.name(), name,
parserFunction parserFunction
); );
} }
@ -410,12 +457,12 @@ public final class AnnotationParser<C> {
) { ) {
final AnnotationAccessor classAnnotations = AnnotationAccessor.of(instance.getClass()); final AnnotationAccessor classAnnotations = AnnotationAccessor.of(instance.getClass());
final CommandMethod classCommandMethod = classAnnotations.annotation(CommandMethod.class); final CommandMethod classCommandMethod = classAnnotations.annotation(CommandMethod.class);
final String syntaxPrefix = classCommandMethod == null ? "" : (classCommandMethod.value() + " "); final String syntaxPrefix = classCommandMethod == null ? "" : (this.processString(classCommandMethod.value()) + " ");
final Collection<Command<C>> commands = new ArrayList<>(); final Collection<Command<C>> commands = new ArrayList<>();
for (final CommandMethodPair commandMethodPair : methodPairs) { for (final CommandMethodPair commandMethodPair : methodPairs) {
final CommandMethod commandMethod = commandMethodPair.getCommandMethod(); final CommandMethod commandMethod = commandMethodPair.getCommandMethod();
final Method method = commandMethodPair.getMethod(); final Method method = commandMethodPair.getMethod();
final String syntax = syntaxPrefix + commandMethod.value(); final String syntax = syntaxPrefix + this.processString(commandMethod.value());
final List<SyntaxFragment> tokens = this.syntaxParser.apply(syntax); final List<SyntaxFragment> tokens = this.syntaxParser.apply(syntax);
/* Determine command name */ /* Determine command name */
final String commandToken = syntax.split(" ")[0].split("\\|")[0]; final String commandToken = syntax.split(" ")[0].split("\\|")[0];
@ -438,9 +485,10 @@ public final class AnnotationParser<C> {
final Map<CommandArgument<C, ?>, String> argumentDescriptions = new HashMap<>(); final Map<CommandArgument<C, ?>, String> argumentDescriptions = new HashMap<>();
/* Go through all annotated parameters and build up the argument tree */ /* Go through all annotated parameters and build up the argument tree */
for (final ArgumentParameterPair argumentPair : arguments) { for (final ArgumentParameterPair argumentPair : arguments) {
final String argumentName = this.processString(argumentPair.argumentName());
final CommandArgument<C, ?> argument = this.buildArgument( final CommandArgument<C, ?> argument = this.buildArgument(
method, method,
this.findSyntaxFragment(tokens, argumentPair.argumentName()), this.findSyntaxFragment(tokens, argumentName),
argumentPair argumentPair
); );
commandArguments.put(argument.getName(), argument); commandArguments.put(argument.getName(), argument);
@ -482,7 +530,7 @@ public final class AnnotationParser<C> {
final CommandPermission commandPermission = getMethodOrClassAnnotation(method, CommandPermission.class); final CommandPermission commandPermission = getMethodOrClassAnnotation(method, CommandPermission.class);
if (commandPermission != null) { if (commandPermission != null) {
builder = builder.permission(commandPermission.value()); builder = builder.permission(this.processString(commandPermission.value()));
} }
if (commandMethod.requiredSender() != Object.class) { if (commandMethod.requiredSender() != Object.class) {
@ -496,7 +544,7 @@ public final class AnnotationParser<C> {
instance, instance,
commandArguments, commandArguments,
method, method,
this.getParameterInjectorRegistry() this /* annotationParser */
); );
/* Create the command execution handler */ /* Create the command execution handler */
@ -542,7 +590,7 @@ public final class AnnotationParser<C> {
/* Check if we need to construct a proxy */ /* Check if we need to construct a proxy */
if (method.isAnnotationPresent(ProxiedBy.class)) { if (method.isAnnotationPresent(ProxiedBy.class)) {
final ProxiedBy proxyAnnotation = method.getAnnotation(ProxiedBy.class); final ProxiedBy proxyAnnotation = method.getAnnotation(ProxiedBy.class);
final String proxy = proxyAnnotation.value(); final String proxy = this.processString(proxyAnnotation.value());
if (proxy.contains(" ")) { if (proxy.contains(" ")) {
throw new IllegalArgumentException("@ProxiedBy proxies may only contain single literals"); throw new IllegalArgumentException("@ProxiedBy proxies may only contain single literals");
} }
@ -605,16 +653,17 @@ public final class AnnotationParser<C> {
))); )));
} }
/* Check whether or not the corresponding method parameter actually exists */ /* Check whether or not the corresponding method parameter actually exists */
final String argumentName = this.processString(argumentPair.argumentName());
if (syntaxFragment == null || syntaxFragment.getArgumentMode() == ArgumentMode.LITERAL) { if (syntaxFragment == null || syntaxFragment.getArgumentMode() == ArgumentMode.LITERAL) {
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(String.format(
"Invalid command argument '%s' in method '%s': " "Invalid command argument '%s' in method '%s': "
+ "Missing syntax mapping", argumentPair.argumentName(), method.getName())); + "Missing syntax mapping", argumentName, method.getName()));
} }
final Argument argument = argumentPair.getArgument(); final Argument argument = argumentPair.getArgument();
/* Create the argument builder */ /* Create the argument builder */
@SuppressWarnings("rawtypes") final CommandArgument.Builder argumentBuilder = CommandArgument.ofType( @SuppressWarnings("rawtypes") final CommandArgument.Builder argumentBuilder = CommandArgument.ofType(
parameter.getType(), parameter.getType(),
argumentPair.argumentName() argumentName
); );
/* Set the argument requirement status */ /* Set the argument requirement status */
if (syntaxFragment.getArgumentMode() == ArgumentMode.OPTIONAL) { if (syntaxFragment.getArgumentMode() == ArgumentMode.OPTIONAL) {

View file

@ -45,9 +45,14 @@ import org.checkerframework.checker.nullness.qual.NonNull;
final class FlagExtractor implements Function<@NonNull Method, Collection<@NonNull CommandFlag<?>>> { final class FlagExtractor implements Function<@NonNull Method, Collection<@NonNull CommandFlag<?>>> {
private final CommandManager<?> commandManager; private final CommandManager<?> commandManager;
private final AnnotationParser<?> annotationParser;
FlagExtractor(final @NonNull CommandManager<?> commandManager) { FlagExtractor(
final @NonNull CommandManager<?> commandManager,
final @NonNull AnnotationParser<?> annotationParser
) {
this.commandManager = commandManager; this.commandManager = commandManager;
this.annotationParser = annotationParser;
} }
@Override @Override
@ -59,11 +64,12 @@ final class FlagExtractor implements Function<@NonNull Method, Collection<@NonNu
continue; continue;
} }
final Flag flag = parameter.getAnnotation(Flag.class); final Flag flag = parameter.getAnnotation(Flag.class);
final String flagName = this.annotationParser.processString(flag.value());
final CommandFlag.Builder<Void> builder = this.commandManager final CommandFlag.Builder<Void> builder = this.commandManager
.flagBuilder(flag.value()) .flagBuilder(this.annotationParser.processString(flagName))
.withDescription(ArgumentDescription.of(flag.description())) .withDescription(ArgumentDescription.of(this.annotationParser.processString(flag.description())))
.withAliases(flag.aliases()) .withAliases(this.annotationParser.processStrings(flag.aliases()))
.withPermission(Permission.of(flag.permission())); .withPermission(Permission.of(this.annotationParser.processString(flag.permission())));
if (parameter.getType().equals(boolean.class)) { if (parameter.getType().equals(boolean.class)) {
flags.add(builder.build()); flags.add(builder.build());
} else { } else {
@ -71,28 +77,30 @@ final class FlagExtractor implements Function<@NonNull Method, Collection<@NonNu
final Collection<Annotation> annotations = Arrays.asList(parameter.getAnnotations()); final Collection<Annotation> annotations = Arrays.asList(parameter.getAnnotations());
final ParserRegistry<?> registry = this.commandManager.getParserRegistry(); final ParserRegistry<?> registry = this.commandManager.getParserRegistry();
final ArgumentParser<?, ?> parser; final ArgumentParser<?, ?> parser;
if (flag.parserName().isEmpty()) { final String parserName = this.annotationParser.processString(flag.parserName());
if (parserName.isEmpty()) {
parser = registry.createParser(token, registry.parseAnnotations(token, annotations)) parser = registry.createParser(token, registry.parseAnnotations(token, annotations))
.orElse(null); .orElse(null);
} else { } else {
parser = registry.createParser(flag.parserName(), registry.parseAnnotations(token, annotations)) parser = registry.createParser(parserName, registry.parseAnnotations(token, annotations))
.orElse(null); .orElse(null);
} }
if (parser == null) { if (parser == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
String.format("Cannot find parser for type '%s' for flag '%s' in method '%s'", String.format("Cannot find parser for type '%s' for flag '%s' in method '%s'",
parameter.getType().getCanonicalName(), flag.value(), method.getName() parameter.getType().getCanonicalName(), flagName, method.getName()
)); ));
} }
final BiFunction<?, @NonNull String, @NonNull List<String>> suggestionProvider; final BiFunction<?, @NonNull String, @NonNull List<String>> suggestionProvider;
if (!flag.suggestions().isEmpty()) { final String suggestions = this.annotationParser.processString(flag.suggestions());
suggestionProvider = registry.getSuggestionProvider(flag.suggestions()).orElse(null); if (!suggestions.isEmpty()) {
suggestionProvider = registry.getSuggestionProvider(suggestions).orElse(null);
} else { } else {
suggestionProvider = null; suggestionProvider = null;
} }
final CommandArgument.Builder argumentBuilder0 = CommandArgument.ofType( final CommandArgument.Builder argumentBuilder0 = CommandArgument.ofType(
parameter.getType(), parameter.getType(),
flag.value() flagName
); );
final CommandArgument.Builder argumentBuilder = argumentBuilder0.asRequired() final CommandArgument.Builder argumentBuilder = argumentBuilder0.asRequired()
.manager(this.commandManager) .manager(this.commandManager)

View file

@ -51,6 +51,7 @@ public class MethodCommandExecutionHandler<C> implements CommandExecutionHandler
private final Parameter[] parameters; private final Parameter[] parameters;
private final MethodHandle methodHandle; private final MethodHandle methodHandle;
private final AnnotationAccessor annotationAccessor; private final AnnotationAccessor annotationAccessor;
private final AnnotationParser<C> annotationParser;
/** /**
* Constructs a new method command execution handler * Constructs a new method command execution handler
@ -65,6 +66,7 @@ public class MethodCommandExecutionHandler<C> implements CommandExecutionHandler
this.methodHandle = MethodHandles.lookup().unreflect(context.method).bindTo(context.instance); this.methodHandle = MethodHandles.lookup().unreflect(context.method).bindTo(context.instance);
this.parameters = context.method.getParameters(); this.parameters = context.method.getParameters();
this.annotationAccessor = AnnotationAccessor.of(context.method); this.annotationAccessor = AnnotationAccessor.of(context.method);
this.annotationParser = context.annotationParser();
} }
/** /**
@ -123,10 +125,11 @@ public class MethodCommandExecutionHandler<C> implements CommandExecutionHandler
} }
} else if (parameter.isAnnotationPresent(Flag.class)) { } else if (parameter.isAnnotationPresent(Flag.class)) {
final Flag flag = parameter.getAnnotation(Flag.class); final Flag flag = parameter.getAnnotation(Flag.class);
final String flagName = this.annotationParser.processString(flag.value());
if (parameter.getType() == boolean.class) { if (parameter.getType() == boolean.class) {
arguments.add(flagContext.isPresent(flag.value())); arguments.add(flagContext.isPresent(flagName));
} else { } else {
arguments.add(flagContext.getValue(flag.value(), null)); arguments.add(flagContext.getValue(flagName, null));
} }
} else { } else {
if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) { if (parameter.getType().isAssignableFrom(commandContext.getSender().getClass())) {
@ -205,18 +208,20 @@ public class MethodCommandExecutionHandler<C> implements CommandExecutionHandler
private final Map<String, CommandArgument<C, ?>> commandArguments; private final Map<String, CommandArgument<C, ?>> commandArguments;
private final Method method; private final Method method;
private final ParameterInjectorRegistry<C> injectorRegistry; private final ParameterInjectorRegistry<C> injectorRegistry;
private final AnnotationParser<C> annotationParser;
CommandMethodContext( CommandMethodContext(
final @NonNull Object instance, final @NonNull Object instance,
final @NonNull Map<@NonNull String, @NonNull CommandArgument<@NonNull C, @NonNull ?>> commandArguments, final @NonNull Map<@NonNull String, @NonNull CommandArgument<@NonNull C, @NonNull ?>> commandArguments,
final @NonNull Method method, final @NonNull Method method,
final @NonNull ParameterInjectorRegistry<C> injectorRegistry final @NonNull AnnotationParser<C> annotationParser
) { ) {
this.instance = instance; this.instance = instance;
this.commandArguments = commandArguments; this.commandArguments = commandArguments;
this.method = method; this.method = method;
this.method.setAccessible(true); this.method.setAccessible(true);
this.injectorRegistry = injectorRegistry; this.injectorRegistry = annotationParser.getParameterInjectorRegistry();
this.annotationParser = annotationParser;
} }
/** /**
@ -259,6 +264,16 @@ public class MethodCommandExecutionHandler<C> implements CommandExecutionHandler
return this.injectorRegistry; return this.injectorRegistry;
} }
/**
* The annotation parser
*
* @return Annotation parser
* @since 1.7.0
*/
public @NonNull AnnotationParser<C> annotationParser() {
return this.annotationParser;
}
} }
} }

View file

@ -0,0 +1,76 @@
//
// 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 java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* {@link StringProcessor} that replaces matches of a given {@link Pattern}.
*
* @since 1.7.0
*/
public class PatternReplacingStringProcessor implements StringProcessor {
private final Pattern pattern;
private final Function<MatchResult, String> replacementProvider;
/**
* Creates a new property replacing string processor.
*
* @param pattern the pattern to search for
* @param replacementProvider function generating the replacement strings
*/
public PatternReplacingStringProcessor(
final @NonNull Pattern pattern,
final @NonNull Function<@NonNull MatchResult, @Nullable String> replacementProvider
) {
this.pattern = pattern;
this.replacementProvider = replacementProvider;
}
/**
* {@inheritDoc}
*/
@Override
public @NonNull String processString(
@NonNull final String input
) {
final Matcher matcher = this.pattern.matcher(input);
// Kind of copied from the JDK 9+ implementation of "Matcher#replaceFirst"
final StringBuffer stringBuffer = new StringBuffer();
while (matcher.find()) {
final String replacement = this.replacementProvider.apply(matcher);
matcher.appendReplacement(stringBuffer, replacement == null ? "$0" : replacement);
}
matcher.appendTail(stringBuffer);
return stringBuffer.toString();
}
}

View file

@ -0,0 +1,69 @@
//
// 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 java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* {@link PropertyReplacementProvider} that replaces all sub-strings with the format
* {@code ${some.property}} with a function-generated string.
*
* @since 1.7.0
*/
public class PropertyReplacingStringProcessor extends PatternReplacingStringProcessor {
public static final Pattern PROPERTY_REGEX = Pattern.compile("\\$\\{(\\S+)}");
/**
* Creates a new property replacing string processor.
*
* @param replacementProvider function generating the replacement strings
*/
public PropertyReplacingStringProcessor(
final @NonNull Function<@NonNull String, @Nullable String> replacementProvider
) {
super(PROPERTY_REGEX, new PropertyReplacementProvider(replacementProvider));
}
private static final class PropertyReplacementProvider implements Function<@NonNull MatchResult, @Nullable String> {
private final Function<String, String> replacementProvider;
private PropertyReplacementProvider(
final @NonNull Function<String, String> replacementProvider
) {
this.replacementProvider = replacementProvider;
}
@Override
public @Nullable String apply(final @NonNull MatchResult matchResult) {
return this.replacementProvider.apply(matchResult.group(1));
}
}
}

View file

@ -0,0 +1,65 @@
//
// 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 org.checkerframework.checker.nullness.qual.NonNull;
/**
* Processor that intercepts all cloud annotation strings.
*
* @since 1.7.0
*/
@FunctionalInterface
public interface StringProcessor {
/**
* Returns a string processor that simply returns the input string.
*
* @return no-op string processor
*/
static @NonNull StringProcessor noOp() {
return new NoOpStringProcessor();
}
/**
* Processes the {@code input} string and returns the processed result.
* <p>
* This should always return a non-{@code null} result. If the input string
* isn't applicable to the processor implementation, the original string should
* be returned.
*
* @param input the input string
* @return the processed string
*/
@NonNull String processString(@NonNull String input);
final class NoOpStringProcessor implements StringProcessor {
@Override
public @NonNull String processString(final @NonNull String input) {
return input;
}
}
}

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> { public class TestCommandManager extends CommandManager<TestCommandSender> {
protected TestCommandManager() { public TestCommandManager() {
super(CommandExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler()); 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
) {
}
}
}

View file

@ -112,6 +112,7 @@ public final class RegexPreprocessor<C> implements BiFunction<@NonNull CommandCo
/** /**
* Exception thrown when input fails regex matching in {@link RegexPreprocessor} * Exception thrown when input fails regex matching in {@link RegexPreprocessor}
*/ */
@SuppressWarnings("serial")
public static final class RegexValidationException extends IllegalArgumentException { public static final class RegexValidationException extends IllegalArgumentException {
private static final long serialVersionUID = 747826566058072233L; private static final long serialVersionUID = 747826566058072233L;

View file

@ -290,6 +290,7 @@ public final class ByteArgument<C> extends CommandArgument<C, Byte> {
/** /**
* Byte parse exception * Byte parse exception
*/ */
@SuppressWarnings("serial")
public static final class ByteParseException extends NumberParseException { public static final class ByteParseException extends NumberParseException {
private static final long serialVersionUID = -4724241304872989208L; private static final long serialVersionUID = -4724241304872989208L;

View file

@ -279,6 +279,7 @@ public final class DoubleArgument<C> extends CommandArgument<C, Double> {
} }
@SuppressWarnings("serial")
public static final class DoubleParseException extends NumberParseException { public static final class DoubleParseException extends NumberParseException {
private static final long serialVersionUID = 1764554911581976586L; private static final long serialVersionUID = 1764554911581976586L;

View file

@ -274,6 +274,7 @@ public final class FloatArgument<C> extends CommandArgument<C, Float> {
} }
@SuppressWarnings("serial")
public static final class FloatParseException extends NumberParseException { public static final class FloatParseException extends NumberParseException {
private static final long serialVersionUID = -1162983846751812292L; private static final long serialVersionUID = -1162983846751812292L;

View file

@ -337,6 +337,7 @@ public final class IntegerArgument<C> extends CommandArgument<C, Integer> {
} }
@SuppressWarnings("serial")
public static final class IntegerParseException extends NumberParseException { public static final class IntegerParseException extends NumberParseException {
private static final long serialVersionUID = -6933923056628373853L; private static final long serialVersionUID = -6933923056628373853L;

View file

@ -282,6 +282,7 @@ public final class LongArgument<C> extends CommandArgument<C, Long> {
} }
@SuppressWarnings("serial")
public static final class LongParseException extends NumberParseException { public static final class LongParseException extends NumberParseException {
private static final long serialVersionUID = 4366856282301198232L; private static final long serialVersionUID = 4366856282301198232L;

View file

@ -279,6 +279,7 @@ public final class ShortArgument<C> extends CommandArgument<C, Short> {
} }
@SuppressWarnings("serial")
public static final class ShortParseException extends NumberParseException { public static final class ShortParseException extends NumberParseException {
private static final long serialVersionUID = -478674263339091032L; private static final long serialVersionUID = -478674263339091032L;

View file

@ -36,7 +36,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* is being inserted into a {@link CommandTree} and an ambiguity * is being inserted into a {@link CommandTree} and an ambiguity
* is detected. * is detected.
*/ */
@SuppressWarnings("unused") @SuppressWarnings({"unused", "serial"})
public final class AmbiguousNodeException extends IllegalStateException { public final class AmbiguousNodeException extends IllegalStateException {
private static final long serialVersionUID = -200207173805584709L; private static final long serialVersionUID = -200207173805584709L;

View file

@ -32,6 +32,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* *
* @since 1.2.0 * @since 1.2.0
*/ */
@SuppressWarnings("serial")
public class CommandExecutionException extends IllegalArgumentException { public class CommandExecutionException extends IllegalArgumentException {
private static final long serialVersionUID = -4785446899438294661L; private static final long serialVersionUID = -4785446899438294661L;

View file

@ -31,7 +31,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
/** /**
* Exception thrown when parsing user input into a command * Exception thrown when parsing user input into a command
*/ */
@SuppressWarnings("unused") @SuppressWarnings({"unused", "serial"})
public class CommandParseException extends IllegalArgumentException { public class CommandParseException extends IllegalArgumentException {
private static final long serialVersionUID = -2415981126382517435L; private static final long serialVersionUID = -2415981126382517435L;

View file

@ -32,6 +32,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
/** /**
* Exception thrown when an invalid command sender tries to execute a command * Exception thrown when an invalid command sender tries to execute a command
*/ */
@SuppressWarnings("serial")
public final class InvalidCommandSenderException extends CommandParseException { public final class InvalidCommandSenderException extends CommandParseException {
private static final long serialVersionUID = 7372142477529875598L; private static final long serialVersionUID = 7372142477529875598L;

View file

@ -31,7 +31,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
* Thrown when a {@link CommandArgument} * Thrown when a {@link CommandArgument}
* that is registered as a leaf node, does not contain an owning {@link Command} * that is registered as a leaf node, does not contain an owning {@link Command}
*/ */
@SuppressWarnings("unused") @SuppressWarnings({"unused", "serial"})
public final class NoCommandInLeafException extends IllegalStateException { public final class NoCommandInLeafException extends IllegalStateException {
private static final long serialVersionUID = 3373529875213310821L; private static final long serialVersionUID = 3373529875213310821L;

View file

@ -33,7 +33,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
* Exception thrown when a command sender misses a permission required * Exception thrown when a command sender misses a permission required
* to execute a {@link Command} * to execute a {@link Command}
*/ */
@SuppressWarnings("unused") @SuppressWarnings({"unused", "serial"})
public class NoPermissionException extends CommandParseException { public class NoPermissionException extends CommandParseException {
private static final long serialVersionUID = 7103413337750692843L; private static final long serialVersionUID = 7103413337750692843L;

View file

@ -29,6 +29,7 @@ import cloud.commandframework.context.CommandContext;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
@SuppressWarnings("serial")
public class ParserException extends IllegalArgumentException { public class ParserException extends IllegalArgumentException {
private static final long serialVersionUID = -4409795575435072170L; private static final long serialVersionUID = -4409795575435072170L;

View file

@ -52,6 +52,7 @@ versions:
jupiterEngine : 5.8.2 jupiterEngine : 5.8.2
mockitoCore : 4.1.0 mockitoCore : 4.1.0
mockitoKotlin : 4.0.0 mockitoKotlin : 4.0.0
mockitoJupiter: 4.5.1
truth : 1.1.3 truth : 1.1.3
# build-logic # build-logic
@ -206,6 +207,10 @@ dependencies:
group: org.mockito.kotlin group: org.mockito.kotlin
name: mockito-kotlin name: mockito-kotlin
version: { ref: mockitoKotlin } version: { ref: mockitoKotlin }
mockitoJupiter:
group: org.mockito
name: mockito-junit-jupiter
version: { ref: mockitoJupiter }
truth: truth:
group: com.google.truth group: com.google.truth
name: truth name: truth