Implement autocomplete and string parsing for config handling

This commit is contained in:
Ben Woo 2023-09-20 16:32:25 +08:00
parent 6611ec4457
commit 6fe87fbc7b
No known key found for this signature in database
GPG Key ID: FB2A3645536E12C8
20 changed files with 501 additions and 116 deletions

View File

@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.api;
import java.util.Collection;
import io.vavr.control.Try;
import org.jvnet.hk2.annotations.Contract;
@ -33,6 +35,15 @@ public interface MVConfig {
*/
NodeGroup getNodes();
/**
* Auto-complete suggestions for a property.
*
* @param name The name of the node.
* @param input The current user input.
* @return A collection of possible string values.
*/
Collection<String> suggestPropertyValues(String name, String input);
/**
* Gets a property from the config.
*
@ -48,10 +59,20 @@ public interface MVConfig {
* @param name The name of the property.
* @param value The value of the property.
* @return An empty {@link Try} if the property was set successfully, otherwise a {@link Try.Failure} with the
* exception explaining why the property could not be set.
* exception explaining why the property could not be set.
*/
Try<Void> setProperty(String name, Object value);
/**
* Sets a string property in the config.
*
* @param name The name of the property.
* @param value The string value of the property.
* @return An empty {@link Try} if the property was set successfully, otherwise a {@link Try.Failure} with the
* exception explaining why the property could not be set.
*/
Try<Void> setPropertyString(String name, String value);
/**
* Sets world access permissions should be enforced.
* @param enforceAccess The new value.

View File

@ -15,7 +15,6 @@ import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.MVCommandIssuer;
import org.mvplugins.multiverse.core.commandtools.MVCommandManager;
import org.mvplugins.multiverse.core.commandtools.MultiverseCommand;
import org.mvplugins.multiverse.core.commandtools.context.MVConfigValue;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.exceptions.MultiverseException;
@ -33,25 +32,25 @@ class ConfigCommand extends MultiverseCommand {
@Subcommand("config")
@CommandPermission("multiverse.core.config")
@CommandCompletion("@mvconfigs")
@Syntax("<name> [new-value]")
@Description("") // TODO: Description
@CommandCompletion("@mvconfigs @mvconfigvalues")
@Syntax("<name> [value]")
@Description("Show or set a config value.")
void onConfigCommand(
MVCommandIssuer issuer,
@Syntax("<name>")
@Description("") // TODO: Description
@Description("The name of the config to set or show.")
String name,
@Optional
@Syntax("[new-value]")
@Description("") // TODO: Description
MVConfigValue value) {
@Syntax("[value]")
@Description("The value to set the config to. If not specified, the current value will be shown.")
String value) {
if (value == null) {
showConfigValue(issuer, name);
return;
}
updateConfigValue(issuer, name, value.getValue());
updateConfigValue(issuer, name, value);
}
private void showConfigValue(MVCommandIssuer issuer, String name) {
@ -60,8 +59,8 @@ class ConfigCommand extends MultiverseCommand {
.onFailure(e -> issuer.sendMessage(e.getMessage()));
}
private void updateConfigValue(MVCommandIssuer issuer, String name, Object value) {
config.setProperty(name, value)
private void updateConfigValue(MVCommandIssuer issuer, String name, String value) {
config.setPropertyString(name, value)
.onSuccess(ignore -> {
config.save();
issuer.sendMessage("Successfully set " + name + " to " + value);

View File

@ -39,6 +39,7 @@ class MVCommandCompletions extends PaperCommandCompletions {
private final MVCommandManager commandManager;
private final WorldManager worldManager;
private final DestinationsProvider destinationsProvider;
private final MVCoreConfig config;
@Inject
MVCommandCompletions(
@ -50,6 +51,7 @@ class MVCommandCompletions extends PaperCommandCompletions {
this.commandManager = mvCommandManager;
this.worldManager = worldManager;
this.destinationsProvider = destinationsProvider;
this.config = config;
registerAsyncCompletion("commands", this::suggestCommands);
registerAsyncCompletion("destinations", this::suggestDestinations);
@ -59,6 +61,7 @@ class MVCommandCompletions extends PaperCommandCompletions {
registerStaticCompletion("gamemodes", suggestEnums(GameMode.class));
registerStaticCompletion("gamerules", this::suggestGamerules);
registerStaticCompletion("mvconfigs", config.getNodes().getNames());
registerAsyncCompletion("mvconfigvalues", this::suggestMVConfigValues);
registerAsyncCompletion("mvworlds", this::suggestMVWorlds);
setDefaultCompletion("destinations", ParsedDestination.class);
@ -118,6 +121,12 @@ class MVCommandCompletions extends PaperCommandCompletions {
return Arrays.stream(GameRule.values()).map(GameRule::getName).collect(Collectors.toList());
}
private Collection<String> suggestMVConfigValues(BukkitCommandCompletionContext context) {
return Try.of(() -> context.getContextValue(String.class))
.map(propertyName -> config.suggestPropertyValues(propertyName, context.getInput()))
.getOrElse(Collections.emptyList());
}
private Collection<String> suggestMVWorlds(BukkitCommandCompletionContext context) {
if (context.hasConfig("playerOnly") && !context.getIssuer().isPlayer()) {
return Collections.emptyList();

View File

@ -9,17 +9,13 @@ import co.aikar.commands.InvalidCommandArgument;
import co.aikar.commands.PaperCommandContexts;
import co.aikar.commands.contexts.ContextResolver;
import com.google.common.base.Strings;
import io.vavr.control.Option;
import jakarta.inject.Inject;
import org.bukkit.GameRule;
import org.bukkit.entity.Player;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.commandtools.context.GameRuleValue;
import org.mvplugins.multiverse.core.commandtools.context.MVConfigValue;
import org.mvplugins.multiverse.core.config.MVCoreConfig;
import org.mvplugins.multiverse.core.configuration.node.Node;
import org.mvplugins.multiverse.core.configuration.node.ValueNode;
import org.mvplugins.multiverse.core.destination.DestinationsProvider;
import org.mvplugins.multiverse.core.destination.ParsedDestination;
import org.mvplugins.multiverse.core.display.filters.ContentFilter;
@ -59,7 +55,6 @@ class MVCommandContexts extends PaperCommandContexts {
registerIssuerAwareContext(LoadedMultiverseWorld.class, this::parseLoadedMultiverseWorld);
registerIssuerAwareContext(LoadedMultiverseWorld[].class, this::parseLoadedMultiverseWorldArray);
registerIssuerAwareContext(MultiverseWorld.class, this::parseMultiverseWorld);
registerContext(MVConfigValue.class, this::parseMVConfigValue);
registerIssuerAwareContext(Player.class, this::parsePlayer);
registerIssuerAwareContext(Player[].class, this::parsePlayerArray);
}
@ -276,39 +271,6 @@ class MVCommandContexts extends PaperCommandContexts {
throw new InvalidCommandArgument("World " + worldName + " is not a loaded multiverse world.");
}
private MVConfigValue parseMVConfigValue(BukkitCommandExecutionContext context) {
String configName = (String) context.getResolvedArg(String.class);
if (Strings.isNullOrEmpty(configName)) {
throw new InvalidCommandArgument("No config name specified.");
}
Option<Node> node = config.getNodes().findNode(configName);
if (node.isEmpty()) {
throw new InvalidCommandArgument("The config " + configName + " is not valid.");
}
String valueString = context.getFirstArg();
if (Strings.isNullOrEmpty(valueString)) {
throw new InvalidCommandArgument("No config value specified.");
}
if (!(node.get() instanceof ValueNode)) {
context.popFirstArg();
return new MVConfigValue(valueString);
}
ContextResolver<?, BukkitCommandExecutionContext> resolver = getResolver(((ValueNode<?>) node.get()).getType());
if (resolver == null) {
context.popFirstArg();
return new MVConfigValue(valueString);
}
Object resolvedValue = resolver.getContext(context);
if (resolvedValue == null) {
throw new InvalidCommandArgument("The config value " + valueString + " is not valid for config " + configName + ".");
}
return new MVConfigValue(resolvedValue);
}
private Player parsePlayer(BukkitCommandExecutionContext context) {
String resolve = context.getFlagValue("resolve", "");

View File

@ -1,13 +0,0 @@
package org.mvplugins.multiverse.core.commandtools.context;
public class MVConfigValue {
private final Object value;
public MVConfigValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}

View File

@ -3,6 +3,7 @@ package org.mvplugins.multiverse.core.config;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Objects;
import com.dumptruckman.minecraft.util.Logging;
@ -112,14 +113,24 @@ public class MVCoreConfig implements MVConfig {
return configNodes.getNodes();
}
@Override
public Collection<String> suggestPropertyValues(String name, String input) {
return configHandle.suggestPropertyValues(name, input);
}
@Override
public Try<Object> getProperty(String name) {
return configHandle.get(name);
return configHandle.getProperty(name);
}
@Override
public Try<Void> setProperty(String name, Object value) {
return configHandle.set(name, value);
return configHandle.setProperty(name, value);
}
@Override
public Try<Void> setPropertyString(String name, String value) {
return configHandle.setPropertyString(name, value);
}
@Override

View File

@ -0,0 +1,77 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.HashMap;
import java.util.Map;
import co.aikar.commands.ACFUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Provides default serializers for common types.
*/
public final class DefaultSerializerProvider {
private static final Map<Class<?>, NodeSerializer<?>> SERIALIZERS = new HashMap<>();
/**
* Adds a default serializer for the given type.
*
* @param type The type.
* @param serializer The serializer.
* @param <T> The type.
*/
public static <T> void addDefaultSerializer(@NotNull Class<T> type, @NotNull NodeSerializer<T> serializer) {
SERIALIZERS.put(type, serializer);
}
/**
* Gets the default serializer for the given type.
*
* @param type The type.
* @param <T> The type.
* @return The default serializer for the given type, or null if no default serializer exists.
*/
public static <T> @Nullable NodeSerializer<T> getDefaultSerializer(Class<T> type) {
if (type.isEnum()) {
// Special case for enums
return (NodeSerializer<T>) ENUM_SERIALIZER;
}
return (NodeSerializer<T>) SERIALIZERS.get(type);
}
private static final NodeSerializer<Enum> ENUM_SERIALIZER = new NodeSerializer<>() {
@Override
public Enum<?> deserialize(Object object, Class<Enum> type) {
return Enum.valueOf(type, String.valueOf(object).toUpperCase());
}
@Override
public Object serialize(Enum object, Class<Enum> type) {
return object.name();
}
};
private static final NodeSerializer<Boolean> BOOLEAN_SERIALIZER = new NodeSerializer<>() {
@Override
public Boolean deserialize(Object object, Class<Boolean> type) {
if (object instanceof Boolean) {
return (Boolean) object;
}
return ACFUtil.isTruthy(String.valueOf(object));
}
@Override
public Object serialize(Boolean object, Class<Boolean> type) {
return object;
}
};
static {
addDefaultSerializer(Boolean.class, BOOLEAN_SERIALIZER);
}
private DefaultSerializerProvider() {
// Prevent instantiation as this is a static utility class
}
}

View File

@ -0,0 +1,74 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.HashMap;
import java.util.Map;
import co.aikar.commands.ACFUtil;
import io.vavr.control.Try;
/**
* Provides default string parsers for common types.
*/
public final class DefaultStringParserProvider {
private static final Map<Class<?>, NodeStringParser<?>> PARSERS = new HashMap<>();
/**
* Adds a default string parser for the given type.
*
* @param clazz The type.
* @param parser The string parser.
*/
public static void addDefaultStringParser(Class<?> clazz, NodeStringParser<?> parser) {
PARSERS.put(clazz, parser);
}
/**
* Gets the default string parser for the given type.
*
* @param clazz The type.
* @param <T> The type.
* @return The default string parser for the given type, or null if no default string parser exists.
*/
public static <T> NodeStringParser<T> getDefaultStringParser(Class<T> clazz) {
if (clazz.isEnum()) {
// Special case for enums
return (NodeStringParser<T>) ENUM_STRING_PARSER;
}
return (NodeStringParser<T>) PARSERS.get(clazz);
}
private static final NodeStringParser<Enum> ENUM_STRING_PARSER = (input, type) -> Try.of(
() -> Enum.valueOf(type, input.toUpperCase()));
private static final NodeStringParser<String> STRING_STRING_PARSER = (input, type) -> Try.of(
() -> input);
private static final NodeStringParser<Boolean> BOOLEAN_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.isTruthy(String.valueOf(input).toLowerCase()));
private static final NodeStringParser<Integer> INTEGER_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseInt(input));
private static final NodeStringParser<Double> DOUBLE_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseDouble(input));
private static final NodeStringParser<Float> FLOAT_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseFloat(input));
private static final NodeStringParser<Long> LONG_STRING_PARSER = (input, type) -> Try.of(
() -> ACFUtil.parseLong(input));
static {
addDefaultStringParser(String.class, STRING_STRING_PARSER);
addDefaultStringParser(Boolean.class, BOOLEAN_STRING_PARSER);
addDefaultStringParser(Integer.class, INTEGER_STRING_PARSER);
addDefaultStringParser(Double.class, DOUBLE_STRING_PARSER);
addDefaultStringParser(Float.class, FLOAT_STRING_PARSER);
addDefaultStringParser(Long.class, LONG_STRING_PARSER);
}
private DefaultStringParserProvider() {
// Prevent instantiation as this is a static utility class
}
}

View File

@ -0,0 +1,65 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.jetbrains.annotations.Nullable;
/**
* Provides default suggestions for common types.
*/
public final class DefaultSuggesterProvider {
private static final Map<Class<?>, NodeSuggester> SUGGESTERS = new HashMap<>();
/**
* Adds a default suggester for the given type.
*
* @param clazz The type.
* @param suggester The suggester.
*/
public static void addDefaultSuggester(Class<?> clazz, NodeSuggester suggester) {
SUGGESTERS.put(clazz, suggester);
}
/**
* Gets the default suggester for the given type.
*
* @param clazz The type.
* @return The default suggester for the given type, or null if no default suggester exists.
*/
public static @Nullable NodeSuggester getDefaultSuggester(Class<?> clazz) {
if (clazz.isEnum()) {
// Special case for enums
return enumSuggester(clazz);
}
return SUGGESTERS.get(clazz);
}
private static NodeSuggester enumSuggester(Class<?> clazz) {
return input -> Arrays.stream(clazz.getEnumConstants())
.map(Object::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
}
private static final NodeSuggester BOOLEAN_SUGGESTER = input -> List.of("true", "false");
private static final NodeSuggester INTEGER_SUGGESTER = input -> IntStream.range(1, 10)
.boxed()
.map(String::valueOf)
.collect(Collectors.toList());
static {
addDefaultSuggester(Boolean.class, BOOLEAN_SUGGESTER);
addDefaultSuggester(Integer.class, INTEGER_SUGGESTER);
}
private DefaultSuggesterProvider() {
// Prevent instantiation as this is a static utility class
}
}

View File

@ -0,0 +1,26 @@
package org.mvplugins.multiverse.core.configuration.functions;
/**
* A function that serializes and deserializes objects to and from YAML.
*
* @param <T> The type of the object to serialize and deserialize.
*/
public interface NodeSerializer<T> {
/**
* Deserializes an object from YAML.
*
* @param object The object to deserialize.
* @param type The type of the object to deserialize.
* @return The deserialized typed value.
*/
T deserialize(Object object, Class<T> type);
/**
* Serializes an object to YAML.
*
* @param object The object to serialize.
* @param type The type of the object to serialize.
* @return The serialized object.
*/
Object serialize(T object, Class<T> type);
}

View File

@ -0,0 +1,22 @@
package org.mvplugins.multiverse.core.configuration.functions;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A function that parses a string into a node value object of type {@link T}.
*
* @param <T> The type of the object to parse.
*/
@FunctionalInterface
public interface NodeStringParser<T> {
/**
* Parses a string into a node value object of type {@link T}.
*
* @param string The string to parse.
* @param type The type of the object to parse.
* @return The parsed object, or {@link Try.Failure} if the string could not be parsed.
*/
@NotNull Try<T> parse(@Nullable String string, @NotNull Class<T> type);
}

View File

@ -0,0 +1,20 @@
package org.mvplugins.multiverse.core.configuration.functions;
import java.util.Collection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A function that suggests possible values for a node value.
*/
@FunctionalInterface
public interface NodeSuggester {
/**
* Suggests possible values for a node value.
*
* @param input The current user input
* @return The possible values.
*/
@NotNull Collection<String> suggest(@Nullable String input);
}

View File

@ -0,0 +1 @@
package org.mvplugins.multiverse.core.configuration.functions;

View File

@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.configuration.handle;
import java.util.Collection;
import java.util.Collections;
import java.util.logging.Logger;
import io.vavr.control.Try;
@ -10,6 +12,7 @@ import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.migration.ConfigMigrator;
import org.mvplugins.multiverse.core.configuration.node.ConfigNodeNotFoundException;
import org.mvplugins.multiverse.core.configuration.node.Node;
import org.mvplugins.multiverse.core.configuration.node.NodeGroup;
import org.mvplugins.multiverse.core.configuration.node.ValueNode;
@ -68,14 +71,55 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
* Auto-complete suggestions for a property.
*
* @param name The name of the node.
* @return The value of the node.
* @param input The current user input.
* @return A collection of possible string values.
*/
public Try<Object> get(@Nullable String name) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.map(node -> get((ValueNode<Object>) node));
public Collection<String> suggestPropertyValues(@Nullable String name, @Nullable String input) {
return findNode(name, ValueNode.class)
.map(node -> node.suggest(input))
.getOrElse(Collections.emptyList());
}
/**
* Gets the value of a node, if the node has a default value, it will be returned if the node is not found.
*
* @param name The name of the node.
* @return The value of the node, or an error if the node was not found.
*/
public Try<Object> getProperty(@Nullable String name) {
return findNode(name, ValueNode.class).map(this::get);
}
/**
* Sets the string value of a node, if the validator is not null, it will be tested first.
*
* @param name The name of the node.
* @param value The string value to set.
* @return Empty try if the value was set, try containing an error otherwise.
*/
public Try<Void> setPropertyString(@Nullable String name, @Nullable String value) {
return findNode(name, ValueNode.class)
.flatMap(node -> node.parseFromString(value)
.flatMap(parsedValue -> set(node, parsedValue)));
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param name The name of the node.
* @param value The value to set.
* @return Empty try if the value was set, try containing an error otherwise.
*/
public Try<Void> setProperty(@Nullable String name, @Nullable Object value) {
return findNode(name, ValueNode.class).flatMap(node -> set(node, value));
}
private <T extends Node> Try<T> findNode(@Nullable String name, @NotNull Class<T> type) {
return nodes.findNode(name, type)
.toTry(() -> new ConfigNodeNotFoundException(name));
}
/**
@ -91,26 +135,13 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
return node.getSerializer().deserialize(config.get(node.getPath(), node.getDefaultValue()), node.getType());
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param name The name of the node.
* @param value The value to set.
* @return True if the value was set, false otherwise.
*/
public Try<Void> set(@Nullable String name, Object value) {
return nodes.findNode(name, ValueNode.class)
.toTry(() -> new ConfigNodeNotFoundException(name))
.flatMap(node -> set(node, value));
}
/**
* Sets the value of a node, if the validator is not null, it will be tested first.
*
* @param node The node to set the value of.
* @param value The value to set.
* @return True if the value was set, false otherwise.
* @param <T> The type of the node value.
* @return Empty try if the value was set, try containing an error otherwise.
*/
public <T> Try<Void> set(@NotNull ValueNode<T> node, T value) {
return node.validate(value).map(ignore -> {
@ -130,8 +161,9 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
* Sets the default value of a node.
*
* @param node The node to set the default value of.
* @param <T> The type of the node value.
*/
public void setDefault(@NotNull ValueNode node) {
public <T> void setDefault(@NotNull ValueNode<T> node) {
config.set(node.getPath(), node.getDefaultValue());
}
@ -141,13 +173,14 @@ public abstract class GenericConfigHandle<C extends ConfigurationSection> {
* @param <C> The configuration type.
* @param <B> The builder type.
*/
public static abstract class Builder<C extends ConfigurationSection, B extends GenericConfigHandle.Builder<C, B>> {
public abstract static class Builder<C extends ConfigurationSection, B extends GenericConfigHandle.Builder<C, B>> {
protected @Nullable Logger logger;
protected @Nullable NodeGroup nodes;
protected @Nullable ConfigMigrator migrator;
protected Builder() {}
protected Builder() {
}
/**
* Sets the logger.

View File

@ -1,5 +1,7 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.Collection;
import java.util.Collections;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
@ -9,6 +11,13 @@ import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.functions.DefaultSerializerProvider;
import org.mvplugins.multiverse.core.configuration.functions.DefaultStringParserProvider;
import org.mvplugins.multiverse.core.configuration.functions.DefaultSuggesterProvider;
import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer;
import org.mvplugins.multiverse.core.configuration.functions.NodeStringParser;
import org.mvplugins.multiverse.core.configuration.functions.NodeSuggester;
/**
* A node that contains a value.
*
@ -33,6 +42,8 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
protected final @Nullable String name;
protected final @NotNull Class<T> type;
protected final @Nullable Supplier<T> defaultValue;
protected final @Nullable NodeSuggester suggester;
protected final @Nullable NodeStringParser<T> stringParser;
protected final @Nullable NodeSerializer<T> serializer;
protected final @Nullable Function<T, Try<Void>> validator;
protected final @Nullable BiConsumer<T, T> onSetValue;
@ -43,6 +54,8 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Nullable String name,
@NotNull Class<T> type,
@Nullable Supplier<T> defaultValue,
@Nullable NodeSuggester suggester,
@Nullable NodeStringParser<T> stringParser,
@Nullable NodeSerializer<T> serializer,
@Nullable Function<T, Try<Void>> validator,
@Nullable BiConsumer<T, T> onSetValue) {
@ -50,7 +63,15 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
this.name = name;
this.type = type;
this.defaultValue = defaultValue;
this.serializer = serializer;
this.suggester = (suggester != null)
? suggester
: DefaultSuggesterProvider.getDefaultSuggester(type);
this.stringParser = (stringParser != null)
? stringParser
: DefaultStringParserProvider.getDefaultStringParser(type);
this.serializer = (serializer != null)
? serializer
: DefaultSerializerProvider.getDefaultSerializer(type);
this.validator = validator;
this.onSetValue = onSetValue;
}
@ -82,6 +103,31 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Collection<String> suggest(@Nullable String input) {
if (suggester != null) {
return suggester.suggest(input);
}
return Collections.emptyList();
}
/**
* {@inheritDoc}
*/
@Override
public @NotNull Try<T> parseFromString(@Nullable String input) {
if (stringParser != null) {
return stringParser.parse(input, type);
}
return Try.failure(new UnsupportedOperationException("No string parser for type " + type.getName()));
}
/**
* {@inheritDoc}
*/
public @Nullable NodeSerializer<T> getSerializer() {
return serializer;
}
@ -114,11 +160,12 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
* @param <B> The type of the builder.
*/
public static class Builder<T, B extends ConfigNode.Builder<T, B>> extends ConfigHeaderNode.Builder<B> {
private static final NodeSerializer<?> ENUM_NODE_SERIALIZER = new EnumNodeSerializer<>();
protected @Nullable String name;
protected @NotNull final Class<T> type;
protected @Nullable Supplier<T> defaultValue;
protected @Nullable NodeSuggester suggester;
protected @Nullable NodeStringParser<T> stringParser;
protected @Nullable NodeSerializer<T> serializer;
protected @Nullable Function<T, Try<Void>> validator;
protected @Nullable BiConsumer<T, T> onSetValue;
@ -133,9 +180,6 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
super(path);
this.name = path;
this.type = type;
if (type.isEnum()) {
this.serializer = (NodeSerializer<T>) ENUM_NODE_SERIALIZER;
}
}
/**
@ -171,6 +215,28 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
return self();
}
/**
* Sets the suggester for this node.
*
* @param suggester The suggester for this node.
* @return This builder.
*/
public @NotNull B suggester(@NotNull NodeSuggester suggester) {
this.suggester = suggester;
return self();
}
/**
* Sets the string parser for this node.
*
* @param stringParser The string parser for this node.
* @return This builder.
*/
public @NotNull B stringParser(@NotNull NodeStringParser<T> stringParser) {
this.stringParser = stringParser;
return self();
}
/**
* Sets the serializer for this node.
*
@ -210,7 +276,7 @@ public class ConfigNode<T> extends ConfigHeaderNode implements ValueNode<T> {
@Override
public @NotNull ConfigNode<T> build() {
return new ConfigNode<>(path, comments.toArray(new String[0]),
name, type, defaultValue,serializer, validator, onSetValue);
name, type, defaultValue, suggester, stringParser, serializer, validator, onSetValue);
}
protected @NotNull B self() {

View File

@ -1,14 +0,0 @@
package org.mvplugins.multiverse.core.configuration.node;
public class EnumNodeSerializer<T extends Enum<T>> implements NodeSerializer<T> {
@Override
public T deserialize(Object object, Class<T> type) {
return Enum.valueOf(type, object.toString().toUpperCase());
}
@Override
public Object serialize(T object, Class<T> type) {
return object.toString();
}
}

View File

@ -1,6 +0,0 @@
package org.mvplugins.multiverse.core.configuration.node;
public interface NodeSerializer<T> {
T deserialize(Object object, Class<T> type);
Object serialize(T object, Class<T> type);
}

View File

@ -1,10 +1,14 @@
package org.mvplugins.multiverse.core.configuration.node;
import java.util.Collection;
import io.vavr.control.Option;
import io.vavr.control.Try;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mvplugins.multiverse.core.configuration.functions.NodeSerializer;
public interface ValueNode<T> extends Node {
/**
@ -28,6 +32,22 @@ public interface ValueNode<T> extends Node {
*/
@Nullable T getDefaultValue();
/**
* Suggests possible string values for this node.
*
* @param input The input string.
* @return A collection of possible string values.
*/
@NotNull Collection<String> suggest(@Nullable String input);
/**
* Parses the given string into a value of type {@link T}. Used for property set by user input.
*
* @param input The string to parse.
* @return The parsed value, or given exception if parsing failed.
*/
@NotNull Try<T> parseFromString(@Nullable String input);
/**
* Gets the serializer for this node.
*

View File

@ -119,11 +119,11 @@ public final class WorldConfig {
}
public Try<Object> getProperty(String name) {
return configHandle.get(name);
return configHandle.getProperty(name);
}
public Try<Void> setProperty(String name, Object value) {
return configHandle.set(name, value);
return configHandle.setProperty(name, value);
}
public boolean getAdjustSpawn() {

View File

@ -80,6 +80,18 @@ class ConfigTest : TestWithMockBukkit() {
assertEquals(1, config.getProperty("global-debug").get())
}
@Test
fun `Updating an existing config property with setPropertyString reflects the changes in getProperty`() {
assertTrue(config.setPropertyString("enforce-access", "true").isSuccess)
assertEquals(true, config.getProperty("enforce-access").get())
assertTrue(config.setPropertyString("first-spawn-location", "world2").isSuccess)
assertEquals("world2", config.getProperty("first-spawn-location").get())
assertTrue(config.setPropertyString("global-debug", "1").isSuccess)
assertEquals(1, config.getProperty("global-debug").get())
}
@Test
fun `Updating a non-existing property with setProperty returns false`() {
assertTrue(config.setProperty("invalid-property", false).isFailure)