-
Notifications
You must be signed in to change notification settings - Fork 0
ArgumentTypes
It is time to understand ArgumentTypes, one of the most important aspect of KWCommands.
Argument types configures the argument parsing behavior, there are some builtin Argument Types: SingleArgumentType, ListArgumentType, PairArgumentType, and so on. The responsibility of ArgumentType is to define:
- ArgumentParser: Converts ArgumentInput into a defined Object.
- Possibilities: Provides the possibilities of input of the ArgumentType.
- defaultValue: The default value of argument, if the argument is absent.
- InputType: The type of argument input.
- type: The target value type that argument translates to.
Evaluates into a single value. In other words, it requests only one argument that matches the ArgumentParser
constraints and translates into only one value. The SingleArgumentType is the default for the majority of ArgumentTypes
provided by CommonArgTypesKt
.
Evaluates into a list of values that respects ArgumentParser constraints. This is used when you want more than one value of the specific ArgumentType
, lets take the example of Basics and change it a bit:
public class KwDocsArgTypes {
public static void main(String[] args) {
CommandProcessor processor = Processors.createCommonProcessor();
Command command = Command.builder()
.name("play")
.addAlias("request")
.description(Text.of("Request a song"))
.staticArguments()
.addArgument(Argument.builder()
.name("song")
.argumentType(
new ListArgumentType<>(
CommonArgTypesKt.enumArgumentType(Music.class),
TypeInfo.builderOf(List.class).of(Music.class).buildGeneric()
)
)
.build()
)
.build()
.handler((commandContainer, informationProviders, resultHandler) -> {
List<String> songs = commandContainer.<List<Music>>getArgumentValue("song").stream()
.map(it -> it.name().toLowerCase())
.collect(Collectors.toList());
System.out.printf("...Requested songs: %s ...%n", String.join(", ", songs));
return Unit.INSTANCE;
})
.build();
processor.getCommandManager().registerCommand(command, new KwDocsArgTypes());
processor.parseAndDispatch("play this is beautiful", null, InformationProvidersVoid.INSTANCE);
}
}
Output: ...Requested songs: this, is, beautiful ...
ListArgumentType will continue to evaluate the argument until it does not respect the constraints of the ArgumentParser
anymore.
MapArgumentType uses an ArgumentType
as a key and another ArgumentType
as a value, providing a way to have key-value pair arguments, lets tweak your example again:
public class KwDocsArgTypes {
public static void main(String[] args) {
CommandProcessor processor = Processors.createCommonProcessor();
Command command = Command.builder()
.name("play")
.addAlias("request")
.description(Text.of("Request a song"))
.staticArguments()
.addArgument(Argument.builder()
.name("song")
.argumentType(
new MapArgumentType<>(
CommonArgTypesKt.enumArgumentType(Music.class),
CommonArgTypesKt.enumArgumentType(CoverAuthor.class),
TypeInfo.builderOf(Map.class).of(Music.class, CoverAuthor.class).buildGeneric()
)
)
.build()
)
.build()
.handler((commandContainer, informationProviders, resultHandler) -> {
List<String> songs = commandContainer.<Map<Music, CoverAuthor>>getArgumentValue("song").entrySet().stream()
.map(it -> "\\" + it.getKey().name().toLowerCase() + "\\ covered by '" + it.getValue().name().toLowerCase() + "'")
.collect(Collectors.toList());
System.out.printf("...Requested songs: %s ...%n", String.join(", ", songs));
return Unit.INSTANCE;
})
.build();
processor.getCommandManager().registerCommand(command, new KwDocsArgTypes());
processor.parseAndDispatch("play {this=master, is=in, beautiful=cover}", null, InformationProvidersVoid.INSTANCE);
}
}
Output: ...Requested songs: \this\ covered by 'master', \is\ covered by 'in', \beautiful\ covered by 'cover' ...
.
MapArgumentType provides an easy way of having key-value pair argument. Also, instead of =
you could use :
, but they are the only supported types of assignment of key-value for Maps. Also you could use "
and '
for providing strings with spaces, for example:
processor.parseAndDispatch("register user {name='User name', email='test@test.test', 'Total time spent'='0m'}", null, InformationProvidersVoid.INSTANCE);
This applies not only to MapArgumentType
, but for other ArgumentTypes too (even the custom ones, which we will discuss later on this guide).
Now lets talk about something a bit more complex, ComplexMapArgumentType (as the name states). The last one ArgumentType we seen, only allows a key and a value of one type, in other words, if you define an ArgumentType X for key, keys could only be of this type, the same applies to value, but sometimes we want to have key and value of different types, is here were ComplexMapArgumentType takes into action.
It is not really Complex as it says, in actually, it uses a List of PairArgumentType which defines the allowed key-value pair ArgumentTypes, lets see it in action:
public class KwDocsComplexMapArgTypes {
public static void main(String[] args) {
CommandProcessor processor = Processors.createCommonProcessor();
Command command = Command.builder()
.name("play")
.addAlias("request")
.description(Text.of("Request a song"))
.staticArguments()
.addArgument(Argument.builder()
.name("song")
.argumentType(
new ComplexMapArgumentType<>(
Arrays.asList(
ArgumentTypeKt.pairArgumentType(
CommonArgTypesKt.enumArgumentType(Music.class),
CommonArgTypesKt.stringArgumentType()
),
ArgumentTypeKt.pairArgumentType(
CommonArgTypesKt.stringArgumentType(),
CommonArgTypesKt.getIntArgumentType()
)
),
TypeInfo.builderOf(Map.class).of(Object.class, Object.class).buildGeneric()
)
)
.build()
)
.build()
.handler((commandContainer, informationProviders, resultHandler) -> {
List<String> songs = commandContainer.<Map<Object, Object>>getArgumentValue("song").entrySet().stream()
.map(it -> {
if (it.getKey() instanceof Music) {
return String.format("Music '%s': %s", ((Music) it.getKey()).name().toLowerCase(), it.getValue());
} else {
return String.format("Word '%s' appears %s times", it.getKey(), it.getValue());
}
})
.collect(Collectors.toList());
for (String song : songs) {
System.out.printf("... %s ...%n", song);
}
return Unit.INSTANCE;
})
.build();
processor.getCommandManager().registerCommand(command, new KwDocsComplexMapArgTypes());
processor.parseAndDispatch("play {beautiful='You are a beautiful beauty command lib', beaut=2}", null, InformationProvidersVoid.INSTANCE);
}
}
Output:
... Music 'beautiful': You are a beautiful beauty command lib ...
... Word 'beaut' appears 2 times ...
ComplexMapArgumentType
could cover most of the cases where you need to have more than one type of key or value (or both), the only drawback is that, if ComplexMapArgumentType
is used to cover too much cases, unexpected behaviors could happen, such as having more than one pair of key-value ArgumentType that could translate the same argument (you could call it a conflict), only one of them will translate, and the other will left with nothing, which could be unexpected. Also, ComplexMapArgumentType
does not have mechanisms to set that some key-value ArgumentType pair is required in order to command execute, for these cases, a custom ArgumentType must be implemented.
Implementing a custom ArgumentType is not hard, but must be made with care, lets say that you want to translate a Map into a Java Object, for example, you want to take this from a command line: register user {name=...,email-..., age=...}
and create an object of the following type:
public class User {
private String name;
private String email;
private int age;
...
}
Yes, you could use ComplexArgumentType
, but it will not allow you to take advantage of all features of KWCommands, such as error handling and command completion, so lets implement our own ArgumentType to translate this.
User2 class
public class User2 {
private final String name;
private final String email;
private final int age;
public User2(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public int getAge() {
return age;
}
}
ArgumentType class
public class UserArgumentType extends ArgumentType<MapInput, User2> {
private final UserArgumentParser userArgumentParser = new UserArgumentParser(this);
public UserArgumentType(@Nullable User2 defaultValue) {
super(defaultValue, MapInputType.INSTANCE, TypeInfo.of(User2.class));
}
@NotNull
@Override
public ArgumentParser<MapInput, User2> getParser() {
return this.userArgumentParser;
}
@NotNull
@Override
// Possibilities of completion for this Argument
public Possibilities getPossibilities() {
List<Pair<? extends Input, ? extends Input>> inputs = Arrays.asList(
new Pair<>(new SingleInput("name"), new SingleInput("")),
new Pair<>(new SingleInput("email"), new SingleInput("")),
new Pair<>(new SingleInput("age"), new SingleInput(""))
);
MapInput mapInput = new MapInput(inputs);
return () -> Collections.singletonList(mapInput);
}
@NotNull
@Override
// Gets the type of list element at position i
public ArgumentType<?, ?> getListType(@NotNull List<? extends Input> parsedInputs, int i) {
return CommonArgTypesKt.getStringArgumentType();
}
@NotNull
@Override
// Gets the type of map key at position i
public ArgumentType<?, ?> getMapKeyType(@NotNull List<? extends Pair<? extends Input, ? extends Input>> parsedPairs, int i) {
return CommonArgTypesKt.getStringArgumentType();
}
@NotNull
@Override
// Gets the type of map value at position i
public ArgumentType<?, ?> getMapValueType(@NotNull Input key, int i) {
if (i == 2) {
return CommonArgTypesKt.getIntArgumentType();
} else {
return CommonArgTypesKt.getStringArgumentType();
}
}
@Override
// Whether there is a type at position i
public boolean hasType(int i) {
return i >= 0 && i < 3;
}
}
ArgumentParser class
public class UserArgumentParser implements ArgumentParser<MapInput, User2> {
private final ArgumentType<MapInput, User2> argumentType;
public UserArgumentParser(ArgumentType<MapInput, User2> argumentType) {
this.argumentType = argumentType;
}
@NotNull
@Override
public ValueOrValidation<User2> parse(@NotNull MapInput mapInput,
@NotNull ValueOrValidationFactory valueOrValidationFactory) {
List<InvalidElement> invalids = new ArrayList<>();
String name = null;
String email = null;
int age = -1;
for (Pair<Input, Input> kvPair : mapInput.getInput()) {
Input key = kvPair.getFirst();
Input value = kvPair.getSecond();
if (!(key instanceof SingleInput)) {
invalids.add(new InvalidElement(key, this.argumentType, this));
}
if (!(value instanceof SingleInput)) {
invalids.add(new InvalidElement(value, this.argumentType, this));
}
if (!key.getContent().equals("name") && !key.getContent().equals("email") && !key.getContent().equals("age")) {
invalids.add(new InvalidElement(key, this.argumentType, this));
}
if (key.getContent().equals("name")) {
name = value.toInputString();
}
if (key.getContent().equals("email")) {
email = value.toInputString();
}
if (key.getContent().equals("age")) {
if (value.getContent().matches("\\d+")) {
age = Integer.parseInt(value.toInputString());
} else {
invalids.add(new InvalidElement(value, this.argumentType, this));
}
}
}
if (invalids.isEmpty()) {
return valueOrValidationFactory.value(new User2(name, email, age));
} else {
return valueOrValidationFactory.invalid(new Validation(invalids));
}
}
}
Test
public class KwDocsCustomArgType {
public static void main(String[] args) {
CommandProcessor processor = Processors.createCommonProcessor();
Command command = Command.builder()
.name("register")
.addAlias("reg")
.description(Text.of("Register user"))
.staticArguments()
.addArgument(Argument.builder()
.name("user")
.argumentType(
new UserArgumentType(null)
)
.build()
)
.build()
.handler((commandContainer, informationProviders, resultHandler) -> {
User2 user = commandContainer.<User2>getArgumentValue("user");
System.out.printf("Registered user '%s' (email: '%s') age: '%s'%n", user.getName(), user.getEmail(), user.getAge());
return Unit.INSTANCE;
})
.build();
processor.getCommandManager().registerCommand(command, new KwDocsCustomArgType());
processor.parseAndDispatch("register {name='H', email='test', age=5}", null, InformationProvidersVoid.INSTANCE);
}
}
Output: Registered user 'H' (email: 'test') age: '5'
.
As you can see, custom ArgumentTypes are not hard to implement, but you need to take care when implementing them.
To understand more about Input
, take a look here