Custom Parameters - adamtomi/grapefruit GitHub Wiki

What Are Parameter Mappers?

When a user enters a command they enter basically an array of strings. Some of them are literal arguments whilst others need to be transformed into objects. Parameter mappers are responsible for doing this transformation as well as providing a list of possible options that can be suggested for the user.

Builtin Mappers

Grapefruit comes with a couple of builtin mappers, however you'll most likely want to create your own ones that better suit your needs. Here is the list of the default mappers:

  • Boolean
  • Byte
  • Short
  • Integer
  • Float
  • Double
  • Long
  • String (Single, @Quotable, @Greedy; you can find out more about the latter two by clicking here)

Both primitives and their object counterparts are supported.

Creating Custom Mappers

So now that you've seen the list of builtin mappers, you're probably thinking about adding your own ones. Let me give you a simple example.

public final class MyMessageKeys {
    /* The message could be something like this: "Could not find user with name '{name}'." */
    public static final MessageKey USER_NOT_FOUND = MessageKey.of("parameter.user-not-found");

    private MyMessageKeys() {}
}

Imagine that we have a UserManager class which stores users mapped to their names. Writing a custom mapper to retrieve users is really simple.

public class UserMapper extends AbstractParamterMapper<CommandSource, User> {
    private final UserManager userManager;

    public UserMapper(final @NotNull UserManager userManager) {
        super(TypeToken.of(User.class));
        this.userManager = requireNonNull(userManager, "userManager cannot be null");
    }

    @Override
    public @NotNull User map(final @NotNull CommandContext<CommandSource> context,
                             final @NotNull Queue<CommandInput> args,
                             final @NotNull AnnotationList modifiers) throws ParameterMappingException {
        final String rawInput = args.element().rawArg();
        return this.userManager.findUser(rawInput)
                .orElseThrow(() -> new ParameterMappingException(Message.of(
                        MyMessageKeys.USER_NOT_FOUND,
                        Template.of("{name}", rawInput)
                )));
    }

    @Override
    public @NotNull List<String> listSuggestions(final @NotNull CommandContext<CommandSource> context,
                                                 final @NotNull String currentArg,
                                                 final @NotNull AnnotationList modifiers) {
        return this.userManager.getKnownUsers()
                .stream()
                .map(User::getName)
                .toList();
    }
}

Every time a string has to be transformed into an object the corresponding mappers's map function is called. It either successfully converts the string or throws a ParameterMappingException indicating that the given argument isn't correct.

Some platforms support listing suggestions (Minecraft would be a great example of that) while the user is typing the command; this feature makes using commands much more seamless. ParameterMapper#listSuggestions is in charge of collecting all the possible options and returning it as a list. There is no need to filter the to-be-returned options here, the dispatcher will do that for you.

Mappers have a third method, called suggestionsNeedValidation. This method determines whether arguments should be validated before returing suggestions for it. By default this method returns true, and in most cases you shouldn't override it to make it return false.

Registering Custom Mappers

The only item left on your TODO list is to register your mapper.

final UserManager userManager = //...
final CommandDispatcher<CommandSource> dispatcher = //...
dispatcher.mappers().registerMapper(new UserMapper(userManager));

Please note that you need to register every custom mapper BEFORE registering any commands, otherwise you'll see lots of errors popping up.

Named Mappers

Let's say you have two or more mapperss for the same type of argument and you need to differentiate between them. If you're in such a situation, first of all you might want to reconsider if you really need more mappers. If the answer is yes, then here's the solution.

final UserManager userManager = //...
final CommandDispatcher<CommandSource> dispatcher = //...
final MapperRegistry<CommandSource> mapperRegistry = dispatcher.mappers();
mapperRegistry.registerMapper(new UserMapper(userManager));
mapperRegistry.registerNamedMapper("other-user-mapper", new OtherUserMapper(userManager));

Now you have two different user mappers. You can specify which one you wish to use by annotating the desired parameter with @Mapper. If the annotation is missing, the default mapper will be used.

@CommandDefinition(route = "demo")
public void onCommand(final @Source CommandSource source,
                      final User user,
                      final @Mapper("other-user-mapper") User otherUser) {}
⚠️ **GitHub.com Fallback** ⚠️