From 9934496e0fd919d97daf1a1ec271c859d9dcd5b4 Mon Sep 17 00:00:00 2001 From: Norbert Dejlich Date: Sat, 2 Nov 2024 23:50:40 +0100 Subject: [PATCH] GH-476 Support type wrappers for native JDA option types. (#476) --- .../ScheduledRequirementResolver.java | 2 - .../jda/JDACommandTranslator.java | 107 +++++++++++------- .../jda/JDAContextTypeMapper.java | 10 ++ .../litecommands/jda/JDAParseableInput.java | 29 ++--- .../litecommands/jda/JDATypeMapper.java | 10 +- .../litecommands/jda/LiteJDAFactory.java | 7 +- 6 files changed, 105 insertions(+), 60 deletions(-) create mode 100644 litecommands-jda/src/dev/rollczi/litecommands/jda/JDAContextTypeMapper.java diff --git a/litecommands-core/src/dev/rollczi/litecommands/requirement/ScheduledRequirementResolver.java b/litecommands-core/src/dev/rollczi/litecommands/requirement/ScheduledRequirementResolver.java index f29a44506..8b9d62107 100644 --- a/litecommands-core/src/dev/rollczi/litecommands/requirement/ScheduledRequirementResolver.java +++ b/litecommands-core/src/dev/rollczi/litecommands/requirement/ScheduledRequirementResolver.java @@ -30,8 +30,6 @@ class ScheduledRequirementResolver { private final BindRegistry bindRegistry; private final Scheduler scheduler; - private final BiMap, ArgumentKey, ParserSet> cachedParserSets = new BiHashMap<>(); - ScheduledRequirementResolver(ContextRegistry contextRegistry, ParserRegistry parserRegistry, BindRegistry bindRegistry, Scheduler scheduler) { this.contextRegistry = contextRegistry; this.parserRegistry = parserRegistry; diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java index 2e7c34ab0..410dff420 100644 --- a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDACommandTranslator.java @@ -15,7 +15,9 @@ import dev.rollczi.litecommands.meta.MetaHolder; import dev.rollczi.litecommands.priority.PrioritizedList; import dev.rollczi.litecommands.range.Range; +import dev.rollczi.litecommands.reflect.type.TypeToken; import dev.rollczi.litecommands.shared.Preconditions; +import java.util.function.Function; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -42,7 +44,8 @@ public class JDACommandTranslator { private static final String DESCRIPTION_NO_GENERATED = "no generated description"; private final Map, JDAType> jdaSupportedTypes = new HashMap<>(); - private final Map, JDATypeOverlay> jdaTypeOverlays = new HashMap<>(); + private final Map, JDAType> jdaTypeOverlays = new HashMap<>(); + private final Map, JDATypeWrapper> jdaTypeWrappers = new HashMap<>(); private final ParserRegistry parserRegistry; @@ -51,12 +54,29 @@ public JDACommandTranslator(ParserRegistry parserRegistry) { } public JDACommandTranslator type(Class type, OptionType optionType, JDATypeMapper mapper) { - jdaSupportedTypes.put(type, new JDAType<>(type, optionType, mapper)); + return this.type(type, optionType, (invocation, option) -> mapper.map(option)); + } + + public JDACommandTranslator type(Class type, OptionType optionType, JDAContextTypeMapper mapper) { + jdaSupportedTypes.put(type, new JDAType<>(optionType, mapper)); return this; } public JDACommandTranslator typeOverlay(Class type, OptionType optionType, JDATypeMapper mapper) { - jdaTypeOverlays.put(type, new JDATypeOverlay<>(type, optionType, mapper)); + jdaTypeOverlays.put(type, new JDAType<>(optionType, mapper)); + return this; + } + + public JDACommandTranslator typeOverlay(Class type, OptionType optionType, JDAContextTypeMapper mapper) { + jdaTypeOverlays.put(type, new JDAType<>(optionType, mapper)); + return this; + } + + /** + * Wrappers are only used for wrapping types that are supported by Discord API, and we want to skip LiteCommands parsing. + */ + public JDACommandTranslator typeWrapper(Class type, Function, TypeToken> unwrapper, Function wrapper) { + jdaTypeWrappers.put(type, new JDATypeWrapper<>(unwrapper, wrapper)); return this; } @@ -176,28 +196,35 @@ private CommandExecutor translateExecutor(CommandRoute boolean isRequired = isRequired(argument); Class parsedType = argument.getType().getRawType(); - if (jdaSupportedTypes.containsKey(parsedType)) { - JDAType jdaType = jdaSupportedTypes.get(parsedType); - OptionType optionType = jdaType.optionType(); + JDATypeWrapper wrapper = jdaTypeWrappers.get(parsedType); - consumer.translate(optionType, jdaType.mapper(), argumentName, description, isRequired, optionType.canSupportChoices()); - continue; + if (wrapper != null) { + parsedType = wrapper.unwrapper(argument.getType()).getRawType(); } - if (jdaTypeOverlays.containsKey(parsedType)) { - JDATypeOverlay jdaTypeOverlay = jdaTypeOverlays.get(parsedType); - OptionType optionType = jdaTypeOverlay.optionType(); - - consumer.translate(optionType, jdaTypeOverlay.mapper(), argumentName, description, isRequired, optionType.canSupportChoices()); - continue; - } + JDAType type = getType(parsedType); + JDAContextTypeMapper mapper = wrapper != null + ? (invocation, option) -> wrapper.wrap(type.mapper().map(invocation, option)) + : type.mapper(); - consumer.translate(OptionType.STRING, option -> option.getAsString(), argumentName, description, isRequired, true); + consumer.translate(type.optionType(), mapper, argumentName, description, isRequired, type.optionType().canSupportChoices()); } return executor; } + private JDAType getType(Class parsedType) { + if (jdaSupportedTypes.containsKey(parsedType)) { + return jdaSupportedTypes.get(parsedType); + } + + if (jdaTypeOverlays.containsKey(parsedType)) { + return jdaTypeOverlays.get(parsedType); + } + + return new JDAType<>(OptionType.STRING, (invocation, option) -> option.getAsString()); + } + private boolean isRequired(Argument argument) { if (argument.hasDefaultValue()) { return false; @@ -215,7 +242,7 @@ private boolean isRequired(Argument argument) { } private interface TranslateExecutorConsumer { - void translate(OptionType optionType, JDATypeMapper mapper, String argName, String description, boolean isRequired, boolean autocomplete); + void translate(OptionType optionType, JDAContextTypeMapper mapper, String argName, String description, boolean isRequired, boolean autocomplete); } JDAParseableInput translateArguments(JDALiteCommand command, SlashCommandInteractionEvent interaction) { @@ -224,7 +251,7 @@ JDAParseableInput translateArguments(JDALiteCommand command, SlashCommandInterac .collect(Collectors.toList()); Map options = interaction.getOptions().stream() - .collect(Collectors.toMap(OptionMapping::getName, option -> option)); + .collect(Collectors.toMap(optionMapping -> optionMapping.getName(), option -> option)); return new JDAParseableInput(routes, options, command); } @@ -235,7 +262,7 @@ JDASuggestionInput translateSuggestions(CommandAutoCompleteInteraction interacti .collect(Collectors.toList()); Map options = interaction.getOptions().stream() - .collect(Collectors.toMap(OptionMapping::getName, option -> option)); + .collect(Collectors.toMap(optionMapping -> optionMapping.getName(), option -> option)); return new JDASuggestionInput(routes, options, interaction.getFocusedOption()); } @@ -260,7 +287,7 @@ Invocation translateInvocation(CommandRoute route, Input argument static final class JDALiteCommand { private final SlashCommandData jdaCommandData; - private final Map> jdaArgumentTypeMappers = new HashMap<>(); + private final Map> jdaArgumentTypeMappers = new HashMap<>(); JDALiteCommand(SlashCommandData jdaCommandData) { this.jdaCommandData = jdaCommandData; @@ -270,17 +297,17 @@ SlashCommandData jdaCommandData() { return jdaCommandData; } - Object mapArgument(JDARoute jdaRoute, OptionMapping option) { - JDATypeMapper typeMapper = jdaArgumentTypeMappers.get(jdaRoute); + Object mapArgument(JDARoute jdaRoute, OptionMapping option, Invocation invocation) { + JDAContextTypeMapper typeMapper = jdaArgumentTypeMappers.get(jdaRoute); if (typeMapper == null) { return null; } - return typeMapper.map(option); + return typeMapper.map(invocation, option); } - void addTypeMapper(JDARoute route, JDATypeMapper mapper) { + void addTypeMapper(JDARoute route, JDAContextTypeMapper mapper) { jdaArgumentTypeMappers.put(route, mapper); } } @@ -324,13 +351,11 @@ public int hashCode() { } - static final class JDAType { - private final Class type; + static class JDAType { private final OptionType optionType; - private final JDATypeMapper mapper; + private final JDAContextTypeMapper mapper; - JDAType(Class type, OptionType optionType, JDATypeMapper mapper) { - this.type = type; + JDAType(OptionType optionType, JDAContextTypeMapper mapper) { this.optionType = optionType; this.mapper = mapper; } @@ -339,28 +364,26 @@ public OptionType optionType() { return optionType; } - public JDATypeMapper mapper() { + public JDAContextTypeMapper mapper() { return mapper; } } - static final class JDATypeOverlay { - private final Class type; - private final OptionType optionType; - private final JDATypeMapper mapper; + static final class JDATypeWrapper { + private final Function, TypeToken> unwrapper; + private final Function wrapper; - JDATypeOverlay(Class type, OptionType optionType, JDATypeMapper mapper) { - this.type = type; - this.optionType = optionType; - this.mapper = mapper; + public JDATypeWrapper(Function, TypeToken> unwrapper, Function wrapper) { + this.unwrapper = unwrapper; + this.wrapper = (Function) wrapper; } - public OptionType optionType() { - return optionType; + public TypeToken unwrapper(TypeToken type) { + return unwrapper.apply((TypeToken) type); } - public JDATypeMapper mapper() { - return mapper; + private T wrap(Object object) { + return wrapper.apply(object); } } diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAContextTypeMapper.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAContextTypeMapper.java new file mode 100644 index 000000000..623168c23 --- /dev/null +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAContextTypeMapper.java @@ -0,0 +1,10 @@ +package dev.rollczi.litecommands.jda; + +import dev.rollczi.litecommands.invocation.Invocation; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; + +public interface JDAContextTypeMapper { + + T map(Invocation invocation, OptionMapping option); + +} diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java index 24fdeb97b..9dad7fd41 100644 --- a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDAParseableInput.java @@ -1,6 +1,5 @@ package dev.rollczi.litecommands.jda; -import dev.rollczi.litecommands.LiteCommandsException; import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.ParseResult; import dev.rollczi.litecommands.argument.parser.input.ParseableInputMatcher; @@ -9,6 +8,7 @@ import dev.rollczi.litecommands.argument.parser.input.ParseableInput; import dev.rollczi.litecommands.invalidusage.InvalidUsage; import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.range.Range; import dev.rollczi.litecommands.reflect.ReflectUtil; import java.util.function.Supplier; import net.dv8tion.jda.api.interactions.commands.OptionMapping; @@ -47,11 +47,21 @@ public ParseResult nextArgument(Invocation invocation, Ar OptionMapping optionMapping = arguments.get(argument.getName()); if (optionMapping == null) { - return ParseResult.failure(InvalidUsage.Cause.MISSING_ARGUMENT); + Parser parser = parserProvider.get(); + Range range = parser.getRange(argument); + ParseResult defaultResult = argument.getDefaultValue() + .orElseGet(() -> ParseResult.failure(InvalidUsage.Cause.MISSING_ARGUMENT)); + + if (range.getMin() == 0) { + return parser.parse(invocation, argument, RawInput.of()) + .mapFailure(failure -> defaultResult); + } + + return defaultResult; } Class type = argument.getType().getRawType(); - Object input = command.mapArgument(toRoute(argument.getName()), optionMapping); + Object input = command.mapArgument(toRoute(argument.getName()), optionMapping, invocation); consumedArguments.add(argument.getName()); @@ -59,18 +69,9 @@ public ParseResult nextArgument(Invocation invocation, Ar return ParseResult.success((T) input); } - try { - Parser parser = parserProvider.get(); + Parser parser = parserProvider.get(); - return parser.parse(invocation, argument, RawInput.of(optionMapping.getAsString().split(" "))); - } - catch (IllegalArgumentException exception) { - if (input != null) { - throw new LiteCommandsException("Input: " + input + " is not instance of " + type.getSimpleName()); - } - - throw new LiteCommandsException("Cannot parse argument: " + argument.getName(), exception); - } + return parser.parse(invocation, argument, RawInput.of(optionMapping.getAsString().split(" "))); } private JDACommandTranslator.JDARoute toRoute(String argumentName) { diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDATypeMapper.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDATypeMapper.java index 510f2db2d..d04266fc5 100644 --- a/litecommands-jda/src/dev/rollczi/litecommands/jda/JDATypeMapper.java +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/JDATypeMapper.java @@ -1,7 +1,15 @@ package dev.rollczi.litecommands.jda; +import dev.rollczi.litecommands.invocation.Invocation; import net.dv8tion.jda.api.interactions.commands.OptionMapping; -public interface JDATypeMapper { +public interface JDATypeMapper extends JDAContextTypeMapper { + T map(OptionMapping option); + + @Override + default T map(Invocation event, OptionMapping option) { + return map(option); + } + } diff --git a/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java b/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java index 8ae11bdd2..a8576d5f9 100644 --- a/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java +++ b/litecommands-jda/src/dev/rollczi/litecommands/jda/LiteJDAFactory.java @@ -11,6 +11,8 @@ import dev.rollczi.litecommands.jda.permission.DiscordPermissionAnnotationProcessor; import dev.rollczi.litecommands.jda.visibility.VisibilityAnnotationProcessor; import dev.rollczi.litecommands.scope.Scope; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.IMentionable; @@ -74,10 +76,13 @@ private static JDACommandTranslator createTranslator(ParserRegistry wrappe .type(Channel.class, OptionType.CHANNEL, option -> option.getAsChannel()) .type(GuildChannel.class, OptionType.CHANNEL, option -> option.getAsChannel()) .type(GuildChannelUnion.class, OptionType.CHANNEL, option -> option.getAsChannel()) + .type(Member.class, OptionType.USER, (option) -> option.getAsMember()) .typeOverlay(Float.class, OptionType.NUMBER, option -> option.getAsString()) .typeOverlay(float.class, OptionType.NUMBER, option -> option.getAsString()) - .typeOverlay(Member.class, OptionType.USER, option -> option.getAsString()) // TODO: Add raw member parer + + .typeWrapper(Optional.class, type -> type.getParameterized(), value -> Optional.of(value)) + .typeWrapper(CompletableFuture.class, type -> type.getParameterized(), value -> CompletableFuture.completedFuture(value)) ; }