cloud.commandframework.arguments.CommandArgument Maven / Gradle / Ivy
Show all versions of cloud-core Show documentation
//
// MIT License
//
// Copyright (c) 2022 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.arguments;
import cloud.commandframework.ArgumentDescription;
import cloud.commandframework.Command;
import cloud.commandframework.CommandManager;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.arguments.parser.ParserParameters;
import cloud.commandframework.context.CommandContext;
import cloud.commandframework.keys.CloudKey;
import cloud.commandframework.keys.CloudKeyHolder;
import cloud.commandframework.keys.SimpleCloudKey;
import io.leangen.geantyref.TypeToken;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.returnsreceiver.qual.This;
/**
* A argument that belongs to a command
*
* @param Command sender type
* @param The type that the argument parses into
*/
@SuppressWarnings("unused")
@API(status = API.Status.STABLE)
public class CommandArgument implements Comparable>, CloudKeyHolder {
/**
* Pattern for command argument names
*/
private static final Pattern NAME_PATTERN = Pattern.compile("[A-Za-z0-9\\-_]+");
/**
* A typed key representing this argument
*/
private final CloudKey key;
/**
* Indicates whether or not the argument is required
* or not. All arguments prior to any other required
* argument must also be required, such that the predicate
* (∀ c_i ∈ required)({c_0, ..., c_i-1} ⊂ required) holds true,
* where {c_0, ..., c_n-1} is the set of command arguments.
*/
private final boolean required;
/**
* The command argument name. This might be exposed
* to command senders and so should be chosen carefully.
*/
private final String name;
/**
* The parser that is used to parse the command input
* into the corresponding command type
*/
private final ArgumentParser parser;
/**
* Default value, will be empty if none was supplied
*/
private final String defaultValue;
/**
* The type that is produces by the argument's parser
*/
private final TypeToken valueType;
/**
* Suggestion provider
*/
private final BiFunction, String, List> suggestionsProvider;
/**
* Argument preprocessors that allows for extensions to existing argument types
* without having to update all parsers
*/
private final Collection,
@NonNull Queue<@NonNull String>, @NonNull ArgumentParseResult>> argumentPreprocessors;
/**
* A description that will be used when registering this argument if no override is provided.
*/
private final ArgumentDescription defaultDescription;
/**
* Whether or not the argument has been used before
*/
private boolean argumentRegistered = false;
private Command owningCommand;
/**
* Construct a new command argument
*
* @param required Whether or not the argument is required
* @param name The argument name
* @param parser The argument parser
* @param defaultValue Default value used when no value is provided by the command sender
* @param valueType Type produced by the parser
* @param suggestionsProvider Suggestions provider
* @param defaultDescription Default description to use when registering
* @param argumentPreprocessors Argument preprocessors
* @since 1.4.0
*/
@API(status = API.Status.STABLE, since = "1.4.0")
public CommandArgument(
final boolean required,
final @NonNull String name,
final @NonNull ArgumentParser parser,
final @NonNull String defaultValue,
final @NonNull TypeToken valueType,
final @Nullable BiFunction, String, List> suggestionsProvider,
final @NonNull ArgumentDescription defaultDescription,
final @NonNull Collection<@NonNull BiFunction<@NonNull CommandContext, @NonNull Queue<@NonNull String>,
@NonNull ArgumentParseResult>> argumentPreprocessors
) {
this.required = required;
this.name = Objects.requireNonNull(name, "Name may not be null");
if (!NAME_PATTERN.asPredicate().test(name)) {
throw new IllegalArgumentException("Name must be alphanumeric");
}
this.parser = Objects.requireNonNull(parser, "Parser may not be null");
this.defaultValue = defaultValue;
this.valueType = valueType;
this.suggestionsProvider = suggestionsProvider == null
? buildDefaultSuggestionsProvider(this)
: suggestionsProvider;
this.defaultDescription = Objects.requireNonNull(defaultDescription, "Default description may not be null");
this.argumentPreprocessors = new LinkedList<>(argumentPreprocessors);
this.key = SimpleCloudKey.of(this.name, this.valueType);
}
/**
* Construct a new command argument
*
* @param required Whether or not the argument is required
* @param name The argument name
* @param parser The argument parser
* @param defaultValue Default value used when no value is provided by the command sender
* @param valueType Type produced by the parser
* @param suggestionsProvider Suggestions provider
* @param argumentPreprocessors Argument preprocessors
*/
public CommandArgument(
final boolean required,
final @NonNull String name,
final @NonNull ArgumentParser parser,
final @NonNull String defaultValue,
final @NonNull TypeToken valueType,
final @Nullable BiFunction, String, List> suggestionsProvider,
final @NonNull Collection<@NonNull BiFunction<@NonNull CommandContext, @NonNull Queue<@NonNull String>,
@NonNull ArgumentParseResult>> argumentPreprocessors
) {
this(
required,
name,
parser,
defaultValue,
valueType,
suggestionsProvider,
ArgumentDescription.empty(),
argumentPreprocessors
);
}
/**
* Construct a new command argument
*
* @param required Whether or not the argument is required
* @param name The argument name
* @param parser The argument parser
* @param defaultValue Default value used when no value is provided by the command sender
* @param valueType Type produced by the parser
* @param suggestionsProvider Suggestions provider
*/
public CommandArgument(
final boolean required,
final @NonNull String name,
final @NonNull ArgumentParser parser,
final @NonNull String defaultValue,
final @NonNull TypeToken valueType,
final @Nullable BiFunction, String, List> suggestionsProvider
) {
this(required, name, parser, defaultValue, valueType, suggestionsProvider, Collections.emptyList());
}
/**
* Construct a new command argument
*
* @param required Whether or not the argument is required
* @param name The argument name
* @param parser The argument parser
* @param defaultValue Default value used when no value is provided by the command sender
* @param valueType Type produced by the parser
* @param suggestionsProvider Suggestions provider
* @param defaultDescription Default description to use when registering
* @since 1.4.0
*/
@API(status = API.Status.STABLE, since = "1.4.0")
public CommandArgument(
final boolean required,
final @NonNull String name,
final @NonNull ArgumentParser parser,
final @NonNull String defaultValue,
final @NonNull TypeToken valueType,
final @Nullable BiFunction, String, List> suggestionsProvider,
final @NonNull ArgumentDescription defaultDescription
) {
this(required, name, parser, defaultValue, valueType, suggestionsProvider, defaultDescription, Collections.emptyList());
}
/**
* Construct a new command argument
*
* @param required Whether or not the argument is required
* @param name The argument name
* @param parser The argument parser
* @param defaultValue Default value used when no value is provided by the command sender
* @param valueType Type produced by the parser
* @param suggestionsProvider Suggestions provider
*/
public CommandArgument(
final boolean required,
final @NonNull String name,
final @NonNull ArgumentParser parser,
final @NonNull String defaultValue,
final @NonNull Class valueType,
final @Nullable BiFunction<@NonNull CommandContext,
@NonNull String, @NonNull List<@NonNull String>> suggestionsProvider
) {
this(required, name, parser, defaultValue, TypeToken.get(valueType), suggestionsProvider);
}
/**
* Construct a new command argument
*
* @param required Whether or not the argument is required
* @param name The argument name
* @param parser The argument parser
* @param defaultValue Default value used when no value is provided by the command sender
* @param valueType Type produced by the parser
* @param suggestionsProvider Suggestions provider
* @param defaultDescription Default description to use when registering
* @since 1.4.0
*/
@API(status = API.Status.STABLE, since = "1.4.0")
public CommandArgument(
final boolean required,
final @NonNull String name,
final @NonNull ArgumentParser parser,
final @NonNull String defaultValue,
final @NonNull Class valueType,
final @Nullable BiFunction<@NonNull CommandContext,
@NonNull String, @NonNull List<@NonNull String>> suggestionsProvider,
final @NonNull ArgumentDescription defaultDescription
) {
this(required, name, parser, defaultValue, TypeToken.get(valueType), suggestionsProvider, defaultDescription);
}
/**
* Construct a new command argument
*
* @param required Whether the argument is required
* @param name The argument name
* @param parser The argument parser
* @param valueType Type produced by the parser
*/
public CommandArgument(
final boolean required,
final @NonNull String name,
final @NonNull ArgumentParser parser,
final @NonNull Class valueType
) {
this(required, name, parser, "", valueType, null);
}
private static @NonNull BiFunction<@NonNull CommandContext, @NonNull String,
@NonNull List> buildDefaultSuggestionsProvider(final @NonNull CommandArgument argument) {
return new DelegatingSuggestionsProvider<>(argument.getName(), argument.getParser());
}
/**
* Create a new command argument
*
* @param clazz Argument class
* @param name Argument name
* @param Command sender type
* @param Argument Type. Used to make the compiler happy.
* @return Argument builder
*/
public static CommandArgument.@NonNull Builder ofType(
final @NonNull TypeToken clazz,
final @NonNull String name
) {
return new Builder<>(clazz, name);
}
/**
* Create a new command argument
*
* @param clazz Argument class
* @param name Argument name
* @param Command sender type
* @param Argument Type. Used to make the compiler happy.
* @return Argument builder
*/
public static CommandArgument.@NonNull Builder<@NonNull C, @NonNull T> ofType(
final @NonNull Class clazz,
final @NonNull String name
) {
return new Builder<>(TypeToken.get(clazz), name);
}
@Override
public final @NonNull CloudKey getKey() {
return this.key;
}
/**
* Check whether or not the command argument is required
*
* @return {@code true} if the argument is required, {@code false} if not
*/
public boolean isRequired() {
return this.required;
}
/**
* Get the command argument name;
*
* @return Argument name
*/
public @NonNull String getName() {
return this.name;
}
/**
* Get the parser that is used to parse the command input
* into the corresponding command type
*
* @return Command parser
*/
public @NonNull ArgumentParser getParser() {
return this.parser;
}
@Override
public final @NonNull String toString() {
return String.format("%s{name=%s}", this.getClass().getSimpleName(), this.name);
}
/**
* Register a new preprocessor. If all preprocessor has succeeding {@link ArgumentParseResult results}
* that all return {@code true}, the argument will be passed onto the parser.
*
* It is important that the preprocessor doesn't pop any input. Instead, it should only peek.
*
* @param preprocessor Preprocessor
* @return {@code this}
*/
public @NonNull @This CommandArgument addPreprocessor(
final @NonNull BiFunction<@NonNull CommandContext, @NonNull Queue,
@NonNull ArgumentParseResult> preprocessor
) {
this.argumentPreprocessors.add(preprocessor);
return this;
}
/**
* Preprocess command input. This will immediately forward any failed argument parse results.
* If none fails, a {@code true} result will be returned
*
* @param context Command context
* @param input Remaining command input. None will be popped
* @return Parsing error, or argument containing {@code true}
*/
public @NonNull ArgumentParseResult preprocess(
final @NonNull CommandContext context,
final @NonNull Queue input
) {
for (final BiFunction<@NonNull CommandContext, @NonNull Queue,
@NonNull ArgumentParseResult> preprocessor : this.argumentPreprocessors) {
final ArgumentParseResult result = preprocessor.apply(
context,
input
);
if (result.getFailure().isPresent()) {
return result;
}
}
return ArgumentParseResult.success(true);
}
/**
* Get the owning command
*
* @return Owning command
*/
public @Nullable Command getOwningCommand() {
return this.owningCommand;
}
/**
* Set the owning command
*
* @param owningCommand Owning command
*/
public void setOwningCommand(final @NonNull Command owningCommand) {
if (this.owningCommand != null) {
throw new IllegalStateException("Cannot replace owning command");
}
this.owningCommand = owningCommand;
}
/**
* Get the argument suggestions provider
*
* @return Suggestions provider
*/
public final @NonNull BiFunction<@NonNull CommandContext, @NonNull String,
@NonNull List> getSuggestionsProvider() {
return this.suggestionsProvider;
}
/**
* Get the default description to use when registering and no other is provided.
*
* @return the default description
*/
public final @NonNull ArgumentDescription getDefaultDescription() {
return this.defaultDescription;
}
@Override
public final boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final CommandArgument, ?> that = (CommandArgument, ?>) o;
return this.isRequired() == that.isRequired() && Objects.equals(this.getName(), that.getName());
}
@Override
public final int hashCode() {
return Objects.hash(this.isRequired(), this.getName());
}
@Override
public final int compareTo(final @NonNull CommandArgument, ?> o) {
if (this instanceof StaticArgument) {
if (o instanceof StaticArgument) {
return this.getName().compareTo(o.getName());
} else {
return -1;
}
} else {
if (o instanceof StaticArgument) {
return 1;
} else {
return 0;
}
}
}
/**
* Get the default value
*
* @return Default value
*/
public @NonNull String getDefaultValue() {
return this.defaultValue;
}
/**
* Check if the argument has a default value
*
* @return {@code true} if the argument has a default value, {@code false} if not
*/
public boolean hasDefaultValue() {
return !this.isRequired()
&& !this.getDefaultValue().isEmpty();
}
/**
* Get the type of this argument's value
*
* @return Value type
*/
public @NonNull TypeToken getValueType() {
return this.valueType;
}
/**
* Create a copy of the command argument
*
* @return Copied argument
*/
public @NonNull CommandArgument copy() {
CommandArgument.Builder builder = ofType(this.valueType, this.name);
builder = builder.withSuggestionsProvider(this.suggestionsProvider);
builder = builder.withParser(this.parser);
if (this.isRequired()) {
builder = builder.asRequired();
} else if (this.defaultValue.isEmpty()) {
builder = builder.asOptional();
} else {
builder = builder.asOptionalWithDefault(this.defaultValue);
}
builder = builder.withDefaultDescription(this.defaultDescription);
return builder.build();
}
/**
* Check whether the argument has been used in a command
*
* @return {@code true} if the argument has been used in a command, else {@code false}
*/
public boolean isArgumentRegistered() {
return this.argumentRegistered;
}
/**
* Indicate that the argument has been associated with a command
*/
public void setArgumentRegistered() {
this.argumentRegistered = true;
}
/**
* Mutable builder for {@link CommandArgument} instances
*
* @param Command sender type
* @param Argument value type
*/
@API(status = API.Status.STABLE)
public static class Builder {
private final TypeToken valueType;
private final String name;
private CommandManager manager;
private boolean required = true;
private ArgumentParser parser;
private String defaultValue = "";
private BiFunction<@NonNull CommandContext, @NonNull String, @NonNull List> suggestionsProvider;
private @NonNull ArgumentDescription defaultDescription = ArgumentDescription.empty();
private final Collection,
@NonNull String, @NonNull ArgumentParseResult>> argumentPreprocessors = new LinkedList<>();
protected Builder(
final @NonNull TypeToken valueType,
final @NonNull String name
) {
this.valueType = valueType;
this.name = name;
}
protected Builder(
final @NonNull Class valueType,
final @NonNull String name
) {
this(TypeToken.get(valueType), name);
}
/**
* Set the command manager. Will be used to create a default parser
* if none was provided
*
* @param manager Command manager
* @return Builder instance
*/
public @NonNull @This Builder<@NonNull C, @NonNull T> manager(final @NonNull CommandManager manager) {
this.manager = manager;
return this;
}
/**
* Indicates that the argument is required.
* All arguments prior to any other required
* argument must also be required, such that the predicate
* (∀ c_i ∈ required)({c_0, ..., c_i-1} ⊂ required) holds true,
* where {c_0, ..., c_n-1} is the set of command arguments.
*
* @return Builder instance
*/
public @NonNull @This Builder<@NonNull C, @NonNull T> asRequired() {
this.required = true;
return this;
}
/**
* Indicates that the argument is optional.
* All arguments prior to any other required
* argument must also be required, such that the predicate
* (∀ c_i ∈ required)({c_0, ..., c_i-1} ⊂ required) holds true,
* where {c_0, ..., c_n-1} is the set of command arguments.
*
* @return Builder instance
*/
public @NonNull @This Builder<@NonNull C, @NonNull T> asOptional() {
this.required = false;
return this;
}
/**
* Indicates that the argument is optional.
* All arguments prior to any other required
* argument must also be required, such that the predicate
* (∀ c_i ∈ required)({c_0, ..., c_i-1} ⊂ required) holds true,
* where {c_0, ..., c_n-1} is the set of command arguments.
*
* @param defaultValue Default value that will be used if none was supplied
* @return Builder instance
*/
public @NonNull @This Builder<@NonNull C, @NonNull T> asOptionalWithDefault(final @NonNull String defaultValue) {
this.defaultValue = defaultValue;
this.required = false;
return this;
}
/**
* Set the argument parser
*
* @param parser Argument parser
* @return Builder instance
*/
public @NonNull @This Builder<@NonNull C, @NonNull T> withParser(final @NonNull ArgumentParser<@NonNull C, @NonNull T> parser) {
this.parser = Objects.requireNonNull(parser, "Parser may not be null");
return this;
}
/**
* Set the suggestions provider
*
* @param suggestionsProvider Suggestions provider
* @return Builder instance
*/
public @NonNull @This Builder<@NonNull C, @NonNull T> withSuggestionsProvider(
final @NonNull BiFunction<@NonNull CommandContext,
@NonNull String, @NonNull List> suggestionsProvider
) {
this.suggestionsProvider = suggestionsProvider;
return this;
}
/**
* Set the default description to be used for this argument.
*
* The default description is used when no other description is provided for a certain argument.
*
* @param defaultDescription The default description
* @return Builder instance
* @since 1.4.0
*/
@API(status = API.Status.STABLE, since = "1.4.0")
public @NonNull @This Builder<@NonNull C, @NonNull T> withDefaultDescription(
final @NonNull ArgumentDescription defaultDescription
) {
this.defaultDescription = Objects.requireNonNull(defaultDescription, "Default description may not be null");
return this;
}
/**
* Construct a command argument from the builder settings
*
* @return Constructed argument
*/
public @NonNull CommandArgument<@NonNull C, @NonNull T> build() {
if (this.parser == null && this.manager != null) {
this.parser = this.manager.parserRegistry().createParser(this.valueType, ParserParameters.empty())
.orElse(null);
}
if (this.parser == null) {
this.parser = (c, i) -> ArgumentParseResult
.failure(new UnsupportedOperationException("No parser was specified"));
}
if (this.suggestionsProvider == null) {
this.suggestionsProvider = new DelegatingSuggestionsProvider<>(this.name, this.parser);
}
return new CommandArgument<>(
this.required,
this.name,
this.parser,
this.defaultValue,
this.valueType,
this.suggestionsProvider,
this.defaultDescription
);
}
protected final @NonNull String getName() {
return this.name;
}
protected final boolean isRequired() {
return this.required;
}
protected final @NonNull ArgumentParser<@NonNull C, @NonNull T> getParser() {
return this.parser;
}
protected final @NonNull String getDefaultValue() {
return this.defaultValue;
}
protected final @NonNull BiFunction<@NonNull CommandContext, @NonNull String, @NonNull List>
getSuggestionsProvider() {
return this.suggestionsProvider;
}
protected final @NonNull ArgumentDescription getDefaultDescription() {
return this.defaultDescription;
}
protected final @NonNull TypeToken getValueType() {
return this.valueType;
}
}
/**
* A variant of builders designed for subclassing, that returns a self type.
*
* @param sender type
* @param argument value type
* @param the subclass type
* @since 1.5.0
*/
@API(status = API.Status.STABLE, since = "1.5.0")
public abstract static class TypedBuilder> extends Builder {
protected TypedBuilder(
final @NonNull TypeToken valueType,
final @NonNull String name
) {
super(valueType, name);
}
protected TypedBuilder(
final @NonNull Class valueType,
final @NonNull String name
) {
super(valueType, name);
}
@SuppressWarnings("unchecked")
protected final B self() {
return (B) this;
}
/**
* {@inheritDoc}
*/
@Override
public @NonNull B manager(final @NonNull CommandManager manager) {
super.manager(manager);
return this.self();
}
/**
* {@inheritDoc}
*/
@Override
public @NonNull B asRequired() {
super.asRequired();
return this.self();
}
/**
* {@inheritDoc}
*/
@Override
public @NonNull B asOptional() {
super.asOptional();
return this.self();
}
/**
* {@inheritDoc}
*/
@Override
public @NonNull B asOptionalWithDefault(final @NonNull String defaultValue) {
super.asOptionalWithDefault(defaultValue);
return this.self();
}
/**
* {@inheritDoc}
*/
@Override
public @NonNull B withParser(final @NonNull ArgumentParser<@NonNull C, @NonNull T> parser) {
super.withParser(parser);
return this.self();
}
/**
* {@inheritDoc}
*/
@Override
public @NonNull Builder<@NonNull C, @NonNull T> withSuggestionsProvider(
final @NonNull BiFunction<@NonNull CommandContext,
@NonNull String, @NonNull List> suggestionsProvider
) {
super.withSuggestionsProvider(suggestionsProvider);
return this.self();
}
}
}