diff --git a/src/main/java/org/mvplugins/multiverse/core/api/MVConfig.java b/src/main/java/org/mvplugins/multiverse/core/api/MVConfig.java index 994d2747..4ba47a79 100644 --- a/src/main/java/org/mvplugins/multiverse/core/api/MVConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/api/MVConfig.java @@ -3,7 +3,7 @@ package org.mvplugins.multiverse.core.api; import io.vavr.control.Try; import org.jvnet.hk2.annotations.Contract; -import org.mvplugins.multiverse.core.configuration.node.NodeGroup; +import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle; import org.mvplugins.multiverse.core.placeholders.MultiverseCorePlaceholders; @Contract @@ -27,30 +27,11 @@ public interface MVConfig { Try save(); /** - * Gets the nodes for the config. + * Gets the handler for managing config with string names and values. * - * @return The nodes for the config. + * @return The config handle for string properties. */ - NodeGroup getNodes(); - - /** - * Gets a property from the config. - * - * @param name The name of the property. - * @return A {@link Try} with the value of the property, otherwise a {@link Try.Failure} if there is no property by - * that name. - */ - Try getProperty(String name); - - /** - * Sets a property in the config. - * - * @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. - */ - Try setProperty(String name, Object value); + StringPropertyHandle getStringPropertyHandle(); /** * Sets world access permissions should be enforced. diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java index b419dc1a..18ac8b06 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/ConfigCommand.java @@ -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,35 +32,35 @@ class ConfigCommand extends MultiverseCommand { @Subcommand("config") @CommandPermission("multiverse.core.config") - @CommandCompletion("@mvconfigs") - @Syntax(" [new-value]") - @Description("") // TODO: Description + @CommandCompletion("@mvconfigs @mvconfigvalues") + @Syntax(" [value]") + @Description("Show or set a config value.") void onConfigCommand( MVCommandIssuer issuer, @Syntax("") - @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) { - config.getProperty(name) + config.getStringPropertyHandle().getProperty(name) .onSuccess(value -> issuer.sendMessage(name + "is currently set to " + value)) .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.getStringPropertyHandle().setPropertyString(name, value) .onSuccess(ignore -> { config.save(); issuer.sendMessage("Successfully set " + name + " to " + value); diff --git a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java index b1f126f9..2a7eda66 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java +++ b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandCompletions.java @@ -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); @@ -58,7 +60,8 @@ class MVCommandCompletions extends PaperCommandCompletions { registerAsyncCompletion("flags", this::suggestFlags); registerStaticCompletion("gamemodes", suggestEnums(GameMode.class)); registerStaticCompletion("gamerules", this::suggestGamerules); - registerStaticCompletion("mvconfigs", config.getNodes().getNames()); + registerStaticCompletion("mvconfigs", config.getStringPropertyHandle().getPropertyNames()); + registerAsyncCompletion("mvconfigvalues", this::suggestMVConfigValues); registerAsyncCompletion("mvworlds", this::suggestMVWorlds); setDefaultCompletion("destinations", ParsedDestination.class); @@ -118,6 +121,13 @@ class MVCommandCompletions extends PaperCommandCompletions { return Arrays.stream(GameRule.values()).map(GameRule::getName).collect(Collectors.toList()); } + private Collection suggestMVConfigValues(BukkitCommandCompletionContext context) { + return Try.of(() -> context.getContextValue(String.class)) + .map(propertyName -> config.getStringPropertyHandle() + .getPropertySuggestedValues(propertyName, context.getInput())) + .getOrElse(Collections.emptyList()); + } + private Collection suggestMVWorlds(BukkitCommandCompletionContext context) { if (context.hasConfig("playerOnly") && !context.getIssuer().isPlayer()) { return Collections.emptyList(); diff --git a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java index d5f38894..1807403e 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/core/commandtools/MVCommandContexts.java @@ -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 = 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 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", ""); diff --git a/src/main/java/org/mvplugins/multiverse/core/commandtools/context/MVConfigValue.java b/src/main/java/org/mvplugins/multiverse/core/commandtools/context/MVConfigValue.java deleted file mode 100644 index e800ca74..00000000 --- a/src/main/java/org/mvplugins/multiverse/core/commandtools/context/MVConfigValue.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/org/mvplugins/multiverse/core/config/MVCoreConfig.java b/src/main/java/org/mvplugins/multiverse/core/config/MVCoreConfig.java index 576e482d..a82f83df 100644 --- a/src/main/java/org/mvplugins/multiverse/core/config/MVCoreConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/config/MVCoreConfig.java @@ -16,13 +16,13 @@ import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.MultiverseCore; import org.mvplugins.multiverse.core.api.MVConfig; import org.mvplugins.multiverse.core.configuration.handle.CommentedYamlConfigHandle; +import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle; import org.mvplugins.multiverse.core.configuration.migration.BooleanMigratorAction; import org.mvplugins.multiverse.core.configuration.migration.ConfigMigrator; import org.mvplugins.multiverse.core.configuration.migration.IntegerMigratorAction; import org.mvplugins.multiverse.core.configuration.migration.InvertBoolMigratorAction; import org.mvplugins.multiverse.core.configuration.migration.MoveMigratorAction; import org.mvplugins.multiverse.core.configuration.migration.VersionMigrator; -import org.mvplugins.multiverse.core.configuration.node.NodeGroup; @Service public class MVCoreConfig implements MVConfig { @@ -32,6 +32,7 @@ public class MVCoreConfig implements MVConfig { private final Path configPath; private final MVCoreConfigNodes configNodes; private final CommentedYamlConfigHandle configHandle; + private final StringPropertyHandle stringPropertyHandle; @Inject MVCoreConfig(@NotNull MultiverseCore core, @NotNull PluginManager pluginManager) { @@ -70,7 +71,7 @@ public class MVCoreConfig implements MVConfig { .build()) .build()) .build(); - + this.stringPropertyHandle = new StringPropertyHandle(configHandle); load(); save(); } @@ -108,18 +109,8 @@ public class MVCoreConfig implements MVConfig { } @Override - public NodeGroup getNodes() { - return configNodes.getNodes(); - } - - @Override - public Try getProperty(String name) { - return configHandle.get(name); - } - - @Override - public Try setProperty(String name, Object value) { - return configHandle.set(name, value); + public StringPropertyHandle getStringPropertyHandle() { + return stringPropertyHandle; } @Override diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultSerializerProvider.java b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultSerializerProvider.java new file mode 100644 index 00000000..a81fcf97 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultSerializerProvider.java @@ -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, NodeSerializer> SERIALIZERS = new HashMap<>(); + + /** + * Adds a default serializer for the given type. + * + * @param type The type. + * @param serializer The serializer. + * @param The type. + */ + public static void addDefaultSerializer(@NotNull Class type, @NotNull NodeSerializer serializer) { + SERIALIZERS.put(type, serializer); + } + + /** + * Gets the default serializer for the given type. + * + * @param type The type. + * @param The type. + * @return The default serializer for the given type, or null if no default serializer exists. + */ + public static @Nullable NodeSerializer getDefaultSerializer(Class type) { + if (type.isEnum()) { + // Special case for enums + return (NodeSerializer) ENUM_SERIALIZER; + } + return (NodeSerializer) SERIALIZERS.get(type); + } + + private static final NodeSerializer ENUM_SERIALIZER = new NodeSerializer<>() { + @Override + public Enum deserialize(Object object, Class type) { + return Enum.valueOf(type, String.valueOf(object).toUpperCase()); + } + + @Override + public Object serialize(Enum object, Class type) { + return object.name(); + } + }; + + private static final NodeSerializer BOOLEAN_SERIALIZER = new NodeSerializer<>() { + @Override + public Boolean deserialize(Object object, Class type) { + if (object instanceof Boolean) { + return (Boolean) object; + } + return ACFUtil.isTruthy(String.valueOf(object)); + } + + @Override + public Object serialize(Boolean object, Class type) { + return object; + } + }; + + static { + addDefaultSerializer(Boolean.class, BOOLEAN_SERIALIZER); + } + + private DefaultSerializerProvider() { + // Prevent instantiation as this is a static utility class + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultStringParserProvider.java b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultStringParserProvider.java new file mode 100644 index 00000000..7543c7de --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultStringParserProvider.java @@ -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, 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 The type. + * @return The default string parser for the given type, or null if no default string parser exists. + */ + public static NodeStringParser getDefaultStringParser(Class clazz) { + if (clazz.isEnum()) { + // Special case for enums + return (NodeStringParser) ENUM_STRING_PARSER; + } + return (NodeStringParser) PARSERS.get(clazz); + } + + private static final NodeStringParser ENUM_STRING_PARSER = (input, type) -> Try.of( + () -> Enum.valueOf(type, input.toUpperCase())); + + private static final NodeStringParser STRING_STRING_PARSER = (input, type) -> Try.of( + () -> input); + + private static final NodeStringParser BOOLEAN_STRING_PARSER = (input, type) -> Try.of( + () -> ACFUtil.isTruthy(String.valueOf(input).toLowerCase())); + + private static final NodeStringParser INTEGER_STRING_PARSER = (input, type) -> Try.of( + () -> ACFUtil.parseInt(input)); + + private static final NodeStringParser DOUBLE_STRING_PARSER = (input, type) -> Try.of( + () -> ACFUtil.parseDouble(input)); + + private static final NodeStringParser FLOAT_STRING_PARSER = (input, type) -> Try.of( + () -> ACFUtil.parseFloat(input)); + + private static final NodeStringParser 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 + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultSuggesterProvider.java b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultSuggesterProvider.java new file mode 100644 index 00000000..62bce4d0 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/DefaultSuggesterProvider.java @@ -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, 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 + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeSerializer.java b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeSerializer.java new file mode 100644 index 00000000..ef3c71ba --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeSerializer.java @@ -0,0 +1,26 @@ +package org.mvplugins.multiverse.core.configuration.functions; + +/** + * A function that serializes and deserializes objects to and from YAML. + * + * @param The type of the object to serialize and deserialize. + */ +public interface NodeSerializer { + /** + * 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 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 type); +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeStringParser.java b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeStringParser.java new file mode 100644 index 00000000..72c83443 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeStringParser.java @@ -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 The type of the object to parse. + */ +@FunctionalInterface +public interface NodeStringParser { + /** + * 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 parse(@Nullable String string, @NotNull Class type); +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeSuggester.java b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeSuggester.java new file mode 100644 index 00000000..43176fbf --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/NodeSuggester.java @@ -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. Generated based on the current user input. + * + * @param input The current partial user input + * @return The possible values. + */ + @NotNull Collection suggest(@Nullable String input); +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/functions/package-info.java b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/package-info.java new file mode 100644 index 00000000..c9e8c3aa --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/functions/package-info.java @@ -0,0 +1 @@ +package org.mvplugins.multiverse.core.configuration.functions; diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java index 8e98f402..a0db2939 100644 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/GenericConfigHandle.java @@ -9,7 +9,6 @@ import org.jetbrains.annotations.NotNull; 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.NodeGroup; import org.mvplugins.multiverse.core.configuration.node.ValueNode; @@ -18,12 +17,12 @@ import org.mvplugins.multiverse.core.configuration.node.ValueNode; */ public abstract class GenericConfigHandle { protected final @Nullable Logger logger; - protected final @Nullable NodeGroup nodes; + protected final @NotNull NodeGroup nodes; protected final @Nullable ConfigMigrator migrator; protected C config; - protected GenericConfigHandle(@Nullable Logger logger, @Nullable NodeGroup nodes, @Nullable ConfigMigrator migrator) { + protected GenericConfigHandle(@Nullable Logger logger, @NotNull NodeGroup nodes, @Nullable ConfigMigrator migrator) { this.logger = logger; this.nodes = nodes; this.migrator = migrator; @@ -67,17 +66,6 @@ public abstract class GenericConfigHandle { }); } - /** - * 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. - */ - public Try get(@Nullable String name) { - return nodes.findNode(name, ValueNode.class) - .toTry(() -> new ConfigNodeNotFoundException(name)) - .map(node -> get((ValueNode) node)); - } - /** * Gets the value of a node, if the node has a default value, it will be returned if the node is not found. * @@ -91,26 +79,13 @@ public abstract class GenericConfigHandle { 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 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 The type of the node value. + * @return Empty try if the value was set, try containing an error otherwise. */ public Try set(@NotNull ValueNode node, T value) { return node.validate(value).map(ignore -> { @@ -126,12 +101,22 @@ public abstract class GenericConfigHandle { }); } + /** + * Gets the configuration. Mainly used for {@link StringPropertyHandle}. + * + * @return The configuration. + */ + @NotNull NodeGroup getNodes() { + return nodes; + } + /** * Sets the default value of a node. * * @param node The node to set the default value of. + * @param The type of the node value. */ - public void setDefault(@NotNull ValueNode node) { + public void setDefault(@NotNull ValueNode node) { config.set(node.getPath(), node.getDefaultValue()); } @@ -141,13 +126,14 @@ public abstract class GenericConfigHandle { * @param The configuration type. * @param The builder type. */ - public static abstract class Builder> { + public abstract static class Builder> { protected @Nullable Logger logger; protected @Nullable NodeGroup nodes; protected @Nullable ConfigMigrator migrator; - protected Builder() {} + protected Builder() { + } /** * Sets the logger. diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/handle/StringPropertyHandle.java b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/StringPropertyHandle.java new file mode 100644 index 00000000..72955a76 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/handle/StringPropertyHandle.java @@ -0,0 +1,99 @@ +package org.mvplugins.multiverse.core.configuration.handle; + +import java.util.Collection; +import java.util.Collections; + +import io.vavr.control.Try; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import org.mvplugins.multiverse.core.configuration.node.ConfigNodeNotFoundException; +import org.mvplugins.multiverse.core.configuration.node.Node; +import org.mvplugins.multiverse.core.configuration.node.ValueNode; + +/** + * Handles setting of config with string names and values. + */ +public class StringPropertyHandle { + private final @NotNull GenericConfigHandle handle; + + /** + * Creates a new string property handle. + * + * @param handle The handle to wrap. + */ + public StringPropertyHandle(@NotNull GenericConfigHandle handle) { + this.handle = handle; + } + + /** + * Gets the names of all properties in this handle. + * + * @return The names of all properties in this handle. + */ + public Collection getPropertyNames() { + return handle.getNodes().getNames(); + } + + /** + * Gets the type of property. + * + * @param name The name of the property. + * @return The type of the property, or an error if the property was not found. + */ + public Try> getPropertyType(@Nullable String name) { + return findNode(name, ValueNode.class).map(ValueNode::getType); + } + + /** + * 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. + */ + public Collection getPropertySuggestedValues(@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 getProperty(@Nullable String name) { + return findNode(name, ValueNode.class).map(node -> handle.get(node)); + } + + /** + * 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 setProperty(@Nullable String name, @Nullable Object value) { + return findNode(name, ValueNode.class).flatMap(node -> handle.set(node, value)); + } + + /** + * 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 setPropertyString(@Nullable String name, @Nullable String value) { + return findNode(name, ValueNode.class) + .flatMap(node -> node.parseFromString(value) + .flatMap(parsedValue -> handle.set(node, parsedValue))); + } + + private Try findNode(@Nullable String name, @NotNull Class type) { + return handle.getNodes().findNode(name, type) + .toTry(() -> new ConfigNodeNotFoundException(name)); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java index 26d0001d..3aed60b5 100644 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ConfigNode.java @@ -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 extends ConfigHeaderNode implements ValueNode { protected final @Nullable String name; protected final @NotNull Class type; protected final @Nullable Supplier defaultValue; + protected final @Nullable NodeSuggester suggester; + protected final @Nullable NodeStringParser stringParser; protected final @Nullable NodeSerializer serializer; protected final @Nullable Function> validator; protected final @Nullable BiConsumer onSetValue; @@ -43,6 +54,8 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { @Nullable String name, @NotNull Class type, @Nullable Supplier defaultValue, + @Nullable NodeSuggester suggester, + @Nullable NodeStringParser stringParser, @Nullable NodeSerializer serializer, @Nullable Function> validator, @Nullable BiConsumer onSetValue) { @@ -50,7 +63,15 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { 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 extends ConfigHeaderNode implements ValueNode { return null; } + /** + * {@inheritDoc} + */ + @Override + public @NotNull Collection suggest(@Nullable String input) { + if (suggester != null) { + return suggester.suggest(input); + } + return Collections.emptyList(); + } + + /** + * {@inheritDoc} + */ + @Override + public @NotNull Try 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 getSerializer() { return serializer; } @@ -114,11 +160,12 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { * @param The type of the builder. */ public static class Builder> extends ConfigHeaderNode.Builder { - private static final NodeSerializer ENUM_NODE_SERIALIZER = new EnumNodeSerializer<>(); protected @Nullable String name; protected @NotNull final Class type; protected @Nullable Supplier defaultValue; + protected @Nullable NodeSuggester suggester; + protected @Nullable NodeStringParser stringParser; protected @Nullable NodeSerializer serializer; protected @Nullable Function> validator; protected @Nullable BiConsumer onSetValue; @@ -133,9 +180,6 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { super(path); this.name = path; this.type = type; - if (type.isEnum()) { - this.serializer = (NodeSerializer) ENUM_NODE_SERIALIZER; - } } /** @@ -171,6 +215,28 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { 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 stringParser) { + this.stringParser = stringParser; + return self(); + } + /** * Sets the serializer for this node. * @@ -210,7 +276,7 @@ public class ConfigNode extends ConfigHeaderNode implements ValueNode { @Override public @NotNull ConfigNode 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() { diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/node/EnumNodeSerializer.java b/src/main/java/org/mvplugins/multiverse/core/configuration/node/EnumNodeSerializer.java deleted file mode 100644 index 9e157412..00000000 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/node/EnumNodeSerializer.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.mvplugins.multiverse.core.configuration.node; - -public class EnumNodeSerializer> implements NodeSerializer { - - @Override - public T deserialize(Object object, Class type) { - return Enum.valueOf(type, object.toString().toUpperCase()); - } - - @Override - public Object serialize(T object, Class type) { - return object.toString(); - } -} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/node/NodeSerializer.java b/src/main/java/org/mvplugins/multiverse/core/configuration/node/NodeSerializer.java deleted file mode 100644 index fbcaa95f..00000000 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/node/NodeSerializer.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.mvplugins.multiverse.core.configuration.node; - -public interface NodeSerializer { - T deserialize(Object object, Class type); - Object serialize(T object, Class type); -} diff --git a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ValueNode.java b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ValueNode.java index d7c2a105..6830d5a7 100644 --- a/src/main/java/org/mvplugins/multiverse/core/configuration/node/ValueNode.java +++ b/src/main/java/org/mvplugins/multiverse/core/configuration/node/ValueNode.java @@ -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 extends Node { /** @@ -28,6 +32,22 @@ public interface ValueNode extends Node { */ @Nullable T getDefaultValue(); + /** + * Suggests possible string values for this node. Generated based on the current user input. + * + * @param input The current partial user input + * @return A collection of possible string values. + */ + @NotNull Collection 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 parseFromString(@Nullable String input); + /** * Gets the serializer for this node. * diff --git a/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java b/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java index b92e4561..0c008763 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/config/WorldConfig.java @@ -16,6 +16,7 @@ import org.jetbrains.annotations.Nullable; import org.mvplugins.multiverse.core.MultiverseCore; import org.mvplugins.multiverse.core.configuration.handle.ConfigurationSectionHandle; +import org.mvplugins.multiverse.core.configuration.handle.StringPropertyHandle; import org.mvplugins.multiverse.core.configuration.migration.BooleanMigratorAction; import org.mvplugins.multiverse.core.configuration.migration.ConfigMigrator; import org.mvplugins.multiverse.core.configuration.migration.IntegerMigratorAction; @@ -33,6 +34,7 @@ public final class WorldConfig { private final String worldName; private final WorldConfigNodes configNodes; private final ConfigurationSectionHandle configHandle; + private final StringPropertyHandle stringPropertyHandle; WorldConfig( @NotNull String worldName, @@ -47,6 +49,7 @@ public final class WorldConfig { .addVersionMigrator(initialVersionMigrator()) .build()) .build(); + this.stringPropertyHandle = new StringPropertyHandle(configHandle); load(); } @@ -119,11 +122,11 @@ public final class WorldConfig { } public Try getProperty(String name) { - return configHandle.get(name); + return stringPropertyHandle.getProperty(name); } public Try setProperty(String name, Object value) { - return configHandle.set(name, value); + return stringPropertyHandle.setProperty(name, value); } public boolean getAdjustSpawn() { diff --git a/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt b/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt index cc30ecbc..becb8953 100644 --- a/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/config/ConfigTest.kt @@ -52,14 +52,14 @@ class ConfigTest : TestWithMockBukkit() { @Test fun `Getting existing config property with getProperty returns expected value`() { - assertEquals(false, config.getProperty("enforce-access").get()) - assertEquals("world", config.getProperty("first-spawn-location").get()) + assertEquals(false, config.stringPropertyHandle.getProperty("enforce-access").get()) + assertEquals("world", config.stringPropertyHandle.getProperty("first-spawn-location").get()) } @Test fun `Getting non-existing config property with getProperty returns null`() { - assertTrue(config.getProperty("invalid-property").isFailure) - assertTrue(config.getProperty("version").isFailure) + assertTrue(config.stringPropertyHandle.getProperty("invalid-property").isFailure) + assertTrue(config.stringPropertyHandle.getProperty("version").isFailure) } @Test @@ -70,19 +70,31 @@ class ConfigTest : TestWithMockBukkit() { @Test fun `Updating an existing config property with setProperty reflects the changes in getProperty`() { - assertTrue(config.setProperty("enforce-access", true).isSuccess) - assertEquals(true, config.getProperty("enforce-access").get()) + assertTrue(config.stringPropertyHandle.setProperty("enforce-access", true).isSuccess) + assertEquals(true, config.stringPropertyHandle.getProperty("enforce-access").get()) - assertTrue(config.setProperty("first-spawn-location", "world2").isSuccess) - assertEquals("world2", config.getProperty("first-spawn-location").get()) + assertTrue(config.stringPropertyHandle.setProperty("first-spawn-location", "world2").isSuccess) + assertEquals("world2", config.stringPropertyHandle.getProperty("first-spawn-location").get()) - assertTrue(config.setProperty("global-debug", 1).isSuccess) - assertEquals(1, config.getProperty("global-debug").get()) + assertTrue(config.stringPropertyHandle.setProperty("global-debug", 1).isSuccess) + assertEquals(1, config.stringPropertyHandle.getProperty("global-debug").get()) + } + + @Test + fun `Updating an existing config property with setPropertyString reflects the changes in getProperty`() { + assertTrue(config.stringPropertyHandle.setPropertyString("enforce-access", "true").isSuccess) + assertEquals(true, config.stringPropertyHandle.getProperty("enforce-access").get()) + + assertTrue(config.stringPropertyHandle.setPropertyString("first-spawn-location", "world2").isSuccess) + assertEquals("world2", config.stringPropertyHandle.getProperty("first-spawn-location").get()) + + assertTrue(config.stringPropertyHandle.setPropertyString("global-debug", "1").isSuccess) + assertEquals(1, config.stringPropertyHandle.getProperty("global-debug").get()) } @Test fun `Updating a non-existing property with setProperty returns false`() { - assertTrue(config.setProperty("invalid-property", false).isFailure) - assertTrue(config.setProperty("version", 1.1).isFailure) + assertTrue(config.stringPropertyHandle.setProperty("invalid-property", false).isFailure) + assertTrue(config.stringPropertyHandle.setProperty("version", 1.1).isFailure) } }