diff --git a/Core/src/main/java/com/songoda/core/configuration/songoda/ConfigEntry.java b/Core/src/main/java/com/songoda/core/configuration/songoda/ConfigEntry.java new file mode 100644 index 00000000..ffe243b7 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/songoda/ConfigEntry.java @@ -0,0 +1,235 @@ +package com.songoda.core.configuration.songoda; + +import com.songoda.core.compatibility.CompatibleMaterial; +import com.songoda.core.configuration.NodeCommentable; +import com.songoda.core.utils.Pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ConfigEntry { + public final @NotNull SongodaYamlConfig config; + public final @NotNull String key; + protected @Nullable Object defaultValue; + + protected @Nullable Map>> upgradeStepsForVersion; + + public ConfigEntry(@NotNull SongodaYamlConfig config, @NotNull String key) { + this(config, key, null); + } + + public ConfigEntry(@NotNull SongodaYamlConfig config, @NotNull String key, @Nullable Object defaultValue) { + this.config = Objects.requireNonNull(config); + this.key = Objects.requireNonNull(key); + this.defaultValue = defaultValue; + + if (get() == null) { + set(this.defaultValue); + } + + this.config.registerConfigEntry(this); + } + + public @Nullable Object getDefaultValue() { + return this.defaultValue; + } + + public void setDefaultValue(@Nullable Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * @see #withComment(Supplier) + */ + public ConfigEntry withComment(String comment) { + return this.withComment(() -> comment); + } + + /** + * @see NodeCommentable#setNodeComment(String, Supplier) + */ + public ConfigEntry withComment(Supplier comment) { + ((NodeCommentable) this.config).setNodeComment(this.key, comment); + + return this; + } + + /** + * @see #withUpgradeStep(int, String, Function) + */ + public ConfigEntry withUpgradeStep(int version, @NotNull String keyInGivenVersion) { + return withUpgradeStep(version, keyInGivenVersion, null); + } + + /** + * @param version The version to upgrade from (e.g. 1 for the upgrade from 1 to 2) + * @param keyInGivenVersion The old key in the given version or null if it didn't change + * @param valueConverter A function that converts the old version's value to a new one, or null if it didn't change + */ + @Contract("_, null, null -> fail") + public ConfigEntry withUpgradeStep(int version, @Nullable String keyInGivenVersion, @Nullable Function valueConverter) { + if (keyInGivenVersion == null && valueConverter == null) { + throw new IllegalArgumentException("You must provide either a key or a value converter"); + } + + if (this.upgradeStepsForVersion == null) { + this.upgradeStepsForVersion = new HashMap<>(1); + } + + this.upgradeStepsForVersion.put(version, new Pair<>(keyInGivenVersion, valueConverter)); + + return this; + } + + /** + * @see SongodaYamlConfig#has(String) + */ + public boolean has() { + return this.config.has(this.key); + } + + /** + * @see SongodaYamlConfig#set(String, Object) + */ + public Object set(@Nullable Object value) { + // TODO: Test what happens if the value is an enum (CompatibleMaterial) + return this.config.set(this.key, value); + } + + /** + * @see SongodaYamlConfig#get(String) + */ + public @Nullable Object get() { + return this.config.get(this.key); + } + + /** + * @see SongodaYamlConfig#getOr(String, Object) + */ + public @Nullable Object getOr(@Nullable Object fallbackValue) { + return this.config.getOr(this.key, fallbackValue); + } + + public @Nullable String getString() { + Object value = get(); + + return value != null ? value.toString() : null; + } + + public @Nullable String getString(String fallbackValue) { + Object value = get(); + + return value == null ? fallbackValue : value.toString(); + } + + /** + * @see #getInt(int) + */ + public int getInt() { + return getInt(0); + } + + /** + * Returns the values parsed as an integer.
+ * If it is a floating point number, it will be rounded down. + * + * @see Double#valueOf(String) + */ + public int getInt(int fallbackValue) { + String value = getString(); + + if (value == null) { + return fallbackValue; + } + + return Double.valueOf(value).intValue(); + } + + /** + * @see #getDouble(double) + */ + public double getDouble() { + return getDouble(0); + } + + /** + * Returns the values parsed as a double. + * + * @see Double#parseDouble(String) + */ + public double getDouble(double fallbackValue) { + String value = getString(); + + if (value == null) { + return fallbackValue; + } + + return Double.parseDouble(value); + } + + /** + * @see #getBoolean(boolean) + */ + public boolean getBoolean() { + return getBoolean(false); + } + + /** + * Returns the values parsed as a boolean. + * + * @see Boolean#parseBoolean(String) + */ + public boolean getBoolean(boolean fallbackValue) { + String value = getString(); + + if (value == null) { + return fallbackValue; + } + + return Boolean.parseBoolean(value); + } + + /** + * @see #getMaterial(CompatibleMaterial) + */ + public CompatibleMaterial getMaterial() { + return getMaterial(null); + } + + /** + * @see CompatibleMaterial#getMaterial(String) + */ + public @Nullable CompatibleMaterial getMaterial(@Nullable CompatibleMaterial defaultValue) { + String value = getString(); + + if (value == null) { + return defaultValue; + } + + return CompatibleMaterial.getMaterial(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ConfigEntry that = (ConfigEntry) o; + + return this.config.equals(that.config) && + this.key.equals(that.key) && + Objects.equals(this.defaultValue, that.defaultValue) && + Objects.equals(this.upgradeStepsForVersion, that.upgradeStepsForVersion); + } + + @Override + public int hashCode() { + return Objects.hash(this.config, this.key, this.defaultValue, this.upgradeStepsForVersion); + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/songoda/SongodaYamlConfig.java b/Core/src/main/java/com/songoda/core/configuration/songoda/SongodaYamlConfig.java new file mode 100644 index 00000000..4f682d4c --- /dev/null +++ b/Core/src/main/java/com/songoda/core/configuration/songoda/SongodaYamlConfig.java @@ -0,0 +1,240 @@ +package com.songoda.core.configuration.songoda; + +import com.songoda.core.configuration.yaml.YamlConfiguration; +import com.songoda.core.utils.Pair; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SongodaYamlConfig extends YamlConfiguration { + protected final String cannotCreateBackupCopyExceptionPrefix = "Unable to create backup copy of config file: "; + + protected final @NotNull File file; + protected final @NotNull Logger logger; + + private int targetVersion; + private ConfigEntry versionEntry; + + protected final Map configEntries = new LinkedHashMap<>(0); + + public SongodaYamlConfig(@NotNull JavaPlugin plugin, @NotNull File file) { + this(file, plugin.getLogger()); + } + + public SongodaYamlConfig(@NotNull JavaPlugin plugin, @NotNull String fileName) { + this(new File(plugin.getDataFolder(), fileName), plugin.getLogger()); + } + + public SongodaYamlConfig(@NotNull File file) { + this(file, null); + } + + public SongodaYamlConfig(@NotNull File file, @Nullable Logger logger) { + super(); + + this.file = Objects.requireNonNull(file); + + if (logger == null) { + logger = Logger.getLogger(getClass().getName()); + } + this.logger = logger; + } + + /** + * Calls {@link #load()} and then {@link #save()}.
+ *
+ * As this is intered to keep the {@link org.bukkit.plugin.java.JavaPlugin#onEnable()} method clean, + * it catches all exceptions and logs them instead.
+ *
+ * If this method returns false, the plugins should be disabled. + * + * @return true if the load and save were successful, false if an exception was thrown. + * + * @see #save() + * @see #load() + */ + public boolean init() { + try { + this.load(); + this.save(); + + return true; + } catch (IOException e) { + this.logger.log(Level.FINER, "Failed to load config file: " + this.file.getPath(), e); + } + + return false; + } + + protected void registerConfigEntry(ConfigEntry entry) { + this.configEntries.put(entry.key, entry); + } + + public void unregisterConfigEntry(ConfigEntry entry) { + unregisterConfigEntry(entry.key); + } + + public void unregisterConfigEntry(String key) { + this.configEntries.remove(key); + } + + public SongodaYamlConfig withVersion(int version) { + return withVersion("version", version, () -> "Don't touch this – it's used to track the version of the config."); + } + + public SongodaYamlConfig withVersion(@NotNull String key, int version, @Nullable Supplier comment) { + if (version < 0) { + throw new IllegalArgumentException("Version must be positive"); + } + + if (this.versionEntry != null) { + this.versionEntry.set(null); + } + + this.targetVersion = version; + + this.versionEntry = new ConfigEntry(this, key, 0); + this.versionEntry.withComment(comment); + this.versionEntry.set(this.targetVersion); + + return this; + } + + public void load() throws IOException { + try (Reader reader = new FileReader(this.file)) { + load(reader); + } catch (FileNotFoundException ignore) { + } catch (IOException e) { + throw new IOException("Unable to load '" + this.file.getPath() + "'", e); + } + } + + public void save() throws IOException { + try (Writer writer = new FileWriter(this.file)) { + super.save(writer); + } catch (IOException e) { + throw new IOException("Unable to save '" + this.file.getPath() + "'", e); + } + } + + @Override + public void load(Reader reader) { + super.load(reader); + + // The interface does not allow to throw an exception, so we log it instead. + try { + upgradeOldConfigVersion(); + } catch (IOException ex) { + if (ex.getMessage().startsWith(this.cannotCreateBackupCopyExceptionPrefix)) { + // Failed to create backup copy, but we can still continue. + this.logger.log(Level.SEVERE, null, ex); + } else { + // This is a real unexpected exception, so we rethrow it. + throw new IllegalStateException(ex); + } + } + + for (ConfigEntry entry : this.configEntries.values()) { + if (entry.get() == null && entry.getDefaultValue() != null) { + entry.set(entry.getDefaultValue()); + } + } + } + + /** + * @return false, if no config version is set or no upgrade is needed + */ + protected boolean upgradeOldConfigVersion() throws IOException { + if (this.versionEntry == null) { + return false; + } + + if (this.versionEntry.getInt() > this.targetVersion) { + throw new IllegalStateException("Cannot upgrade a config version that is higher than the target version"); + } + if (this.versionEntry.getInt() == this.targetVersion) { + return false; + } + + createBackupCopyFile(); + + while (this.versionEntry.getInt() < this.targetVersion) { + upgradeOldConfigVersionByOne(); + } + + return true; + } + + protected void upgradeOldConfigVersionByOne() { + int currentVersion = this.versionEntry.getInt(); + int targetVersion = currentVersion + 1; + + if (targetVersion > this.targetVersion) { + throw new IllegalStateException("Cannot upgrade a config version that is higher than the target version"); + } + + for (ConfigEntry entry : this.configEntries.values()) { + if (entry.upgradeStepsForVersion == null) { + continue; + } + + Pair<@Nullable String, @Nullable Function> upgradeStep = entry.upgradeStepsForVersion.get(currentVersion); + if (upgradeStep == null) { + continue; + } + + String oldEntryKey = upgradeStep.getFirst(); + if (oldEntryKey == null) { + oldEntryKey = entry.key; + } + + Object newValue = get(oldEntryKey); + if (upgradeStep.getSecond() != null) { + newValue = upgradeStep.getSecond().apply(newValue); + } + + set(oldEntryKey, null); + entry.set(newValue); + } + + this.versionEntry.set(targetVersion); + } + + protected void createBackupCopyFile() throws IOException { + if (!this.file.exists()) { + return; + } + + try { + Path targetPath = this.file.toPath().resolveSibling(this.file.getPath() + ".backup" + System.currentTimeMillis()); + + Files.copy( + this.file.toPath(), + targetPath, + StandardCopyOption.REPLACE_EXISTING + ); + + this.logger.warning("Created backup copy of config file '" + this.file.getPath() + "' to '" + targetPath + "'"); + } catch (IOException ex) { + throw new IOException(this.cannotCreateBackupCopyExceptionPrefix + this.file.getPath(), ex); + } + } +} diff --git a/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java index 6690003a..d50c97ce 100644 --- a/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java +++ b/Core/src/main/java/com/songoda/core/configuration/yaml/YamlConfiguration.java @@ -28,6 +28,8 @@ import java.util.Objects; import java.util.Set; import java.util.function.Supplier; +// TODO: Allow registering own custom value converter (e.g. Bukkit-Location to Map and back) +// + move the huge block from #set into such a converter and register it by default public class YamlConfiguration implements IConfiguration, HeaderCommentable, NodeCommentable { protected final @NotNull Yaml yaml; protected final @NotNull DumperOptions yamlDumperOptions; diff --git a/Core/src/main/java/com/songoda/core/utils/Pair.java b/Core/src/main/java/com/songoda/core/utils/Pair.java new file mode 100644 index 00000000..93a255b9 --- /dev/null +++ b/Core/src/main/java/com/songoda/core/utils/Pair.java @@ -0,0 +1,19 @@ +package com.songoda.core.utils; + +public class Pair { + private final T first; + private final U second; + + public Pair(T first, U second) { + this.first = first; + this.second = second; + } + + public T getFirst() { + return this.first; + } + + public U getSecond() { + return this.second; + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/songoda/ConfigEntryTest.java b/Core/src/test/java/com/songoda/core/configuration/songoda/ConfigEntryTest.java new file mode 100644 index 00000000..f1cdf749 --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/songoda/ConfigEntryTest.java @@ -0,0 +1,192 @@ +package com.songoda.core.configuration.songoda; + +import com.songoda.core.compatibility.CompatibleMaterial; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConfigEntryTest { + @Test + void testGetDefaultValue() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key", "value"); + + assertEquals("value", entry.getDefaultValue()); + + entry.setDefaultValue("new-value"); + assertEquals("new-value", entry.getDefaultValue()); + } + + @Test + void testGetOr() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key", "value"); + + assertEquals("value", entry.getOr("invalid")); + + entry.set(null); + assertEquals("invalid", entry.getOr("invalid")); + } + + @Test + void testGetString() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key"); + + entry.set("value"); + assertEquals("value", entry.getString()); + + entry.set("new-value"); + assertEquals("new-value", entry.getString()); + + entry.set(null); + assertNull(entry.getString()); + assertNull(entry.getString(null)); + assertEquals("12", entry.getString("12")); + + entry.set(10.5); + assertEquals("10.5", entry.getString()); + + entry.set(true); + assertEquals("true", entry.getString()); + + entry.set(CompatibleMaterial.STONE); + assertEquals("STONE", entry.getString()); + } + + @Test + void testGetInt() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key"); + + entry.set(1.0); + assertEquals(1, entry.getInt()); + + entry.set("1.5"); + assertEquals(1, entry.getInt()); + + entry.set("10"); + assertEquals(10.0, entry.getInt()); + + entry.set("10,0"); + assertThrows(NumberFormatException.class, entry::getInt); + + entry.set(null); + assertEquals(0, entry.getInt()); + assertEquals(11, entry.getInt(11)); + } + + @Test + void testGetDouble() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key"); + + entry.set(1.0); + assertEquals(1.0, entry.getDouble()); + + entry.set("1.5"); + assertEquals(1.5, entry.getDouble()); + + entry.set("10"); + assertEquals(10.0, entry.getDouble()); + + entry.set("10,0"); + assertThrows(NumberFormatException.class, entry::getDouble); + + entry.set(null); + assertEquals(0.0, entry.getDouble()); + assertEquals(11.5, entry.getDouble(11.5)); + } + + @Test + void testGetBoolean() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key"); + + entry.set(false); + assertFalse(entry.getBoolean()); + + entry.set("false"); + assertFalse(entry.getBoolean()); + + entry.set("invalid"); + assertFalse(entry.getBoolean()); + + entry.set(1); + assertFalse(entry.getBoolean()); + + entry.set(true); + assertTrue(entry.getBoolean()); + + entry.set("true"); + assertTrue(entry.getBoolean()); + + entry.set(null); + assertFalse(entry.getBoolean()); + assertTrue(entry.getBoolean(true)); + } + + + @Test + void testGetMaterial() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key"); + + entry.set("LOG"); + assertEquals(CompatibleMaterial.BIRCH_LOG, entry.getMaterial()); + + entry.set("OAK_LOG"); + assertEquals(CompatibleMaterial.OAK_LOG, entry.getMaterial()); + + entry.set("10"); + assertNull(entry.getMaterial()); + + entry.set(null); + assertNull(entry.getMaterial()); + assertEquals(CompatibleMaterial.ACACIA_BOAT, entry.getMaterial(CompatibleMaterial.ACACIA_BOAT)); + } + + @Test + void testInvalidWithUpgradeNull() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key", "value"); + + assertThrows(IllegalArgumentException.class, () -> entry.withUpgradeStep(1, null, null)); + } + + @Test + void testEqualsAndHashCode() { + SongodaYamlConfig cfg = new SongodaYamlConfig(new File("ConfigEntryTest.yml")); + ConfigEntry entry = new ConfigEntry(cfg, "key", "value"); + + assertEquals(entry, entry); + assertEquals(entry.hashCode(), entry.hashCode()); + + ConfigEntry other = new ConfigEntry(cfg, "key", "value"); + assertEquals(entry, other); + assertEquals(entry.hashCode(), other.hashCode()); + + other = new ConfigEntry(cfg, "key", "value2"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + + other = new ConfigEntry(cfg, "key2", "value"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + + other = new ConfigEntry(cfg, "key", "value2"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + + other = new ConfigEntry(cfg, "key2", "value2"); + assertNotEquals(entry, other); + assertNotEquals(entry.hashCode(), other.hashCode()); + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigRoundtripTest.java b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigRoundtripTest.java new file mode 100644 index 00000000..f2b08089 --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigRoundtripTest.java @@ -0,0 +1,93 @@ +package com.songoda.core.configuration.songoda; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SongodaYamlConfigRoundtripTest { + Path cfg; + + @BeforeEach + void setUp() throws IOException { + Path path = Files.createTempFile("SongodaYamlConfigTest", "yml"); + File file = path.toFile(); + file.deleteOnExit(); + + this.cfg = path; + } + + @AfterEach + void tearDown() throws IOException { + Files.deleteIfExists(this.cfg); + } + + @Test + void roundtripTest() throws IOException { + Files.write(this.cfg, ("# Don't touch this – it's used to track the version of the config.\n" + + "version: 1\n" + + "messages:\n" + + " # This message is shown when the 'foo' command succeeds.\n" + + " fooSuccess: Remastered success value\n" + + "# This is the range of the 'foo' command\n").getBytes()); + + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()) + .withVersion(3); + + ConfigEntry cmdFooSuccess = new ConfigEntry(cfg, "command.foo.success", "Default success value") + .withComment("This message is shown when the 'foo' command succeeds.") + .withUpgradeStep(1, "messages.fooSuccess"); + ConfigEntry range = new ConfigEntry(cfg, "range") + .withComment("This is the range of the 'foo' command") + .withUpgradeStep(1, null, o -> { + if (o == null) { + return 10; + } + + return o; + }) + .withUpgradeStep(2, null, o -> o + " blocks"); + ConfigEntry incrementer = new ConfigEntry(cfg, "incrementer", 0) + .withComment("This is the incrementer of the 'foo' command") + .withUpgradeStep(1, null, o -> { + if (o == null) { + return null; + } + + return (int) o + 1; + }) + .withUpgradeStep(3, null, (o) -> "text"); + + assertTrue(cfg.init()); + + assertNull(cfg.get("messages.fooSuccess")); + assertEquals("Remastered success value", cfg.get("command.foo.success")); + assertEquals("Remastered success value", cmdFooSuccess.get()); + assertTrue(cmdFooSuccess.has()); + + assertTrue(range.has()); + assertEquals(cfg.get("range"), range.get()); + + assertTrue(incrementer.has()); + assertEquals(cfg.get("incrementer"), incrementer.get()); + + assertEquals("# Don't touch this – it's used to track the version of the config.\n" + + "version: 3\n" + + "command:\n" + + " foo:\n" + + " # This message is shown when the 'foo' command succeeds.\n" + + " success: Remastered success value\n" + + "# This is the range of the 'foo' command\n" + + "range: 10 blocks\n" + + "# This is the incrementer of the 'foo' command\n" + + "incrementer: 0\n", new String(Files.readAllBytes(this.cfg))); + } +} diff --git a/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigTest.java b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigTest.java new file mode 100644 index 00000000..78435f4f --- /dev/null +++ b/Core/src/test/java/com/songoda/core/configuration/songoda/SongodaYamlConfigTest.java @@ -0,0 +1,134 @@ +package com.songoda.core.configuration.songoda; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +class SongodaYamlConfigTest { + Path cfg; + + @BeforeEach + void setUp() throws IOException { + this.cfg = createTmpFile(); + } + + @AfterEach + void tearDown() throws IOException { + Files.deleteIfExists(this.cfg); + } + + @Test + void testLoad() throws IOException { + Files.write(this.cfg, "test-key: foo\n".getBytes()); + + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + cfg.set("test-key", "bar"); + cfg.load(); + + assertEquals("foo", cfg.get("test-key")); + } + + @Test + void testSave() throws IOException { + Files.write(this.cfg, "test-key: foo\n".getBytes()); + + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + cfg.set("test-key", "bar"); + cfg.save(); + + assertEquals("test-key: bar\n", new String(Files.readAllBytes(this.cfg))); + } + + @Test + void testWithVersion() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + cfg.withVersion("version-key", 1, null); + + assertEquals(1, cfg.get("version-key")); + + cfg.save(); + assertEquals("version-key: 1\n", new String(Files.readAllBytes(this.cfg))); + + cfg.withVersion(2); + + assertEquals(2, cfg.get("version")); + assertNull(cfg.get("version-key")); + + cfg.save(); + assertEquals( + "# Don't touch this – it's used to track the version of the config.\n" + + "version: 2\n", + new String(Files.readAllBytes(this.cfg)) + ); + } + + @Test + void testWithNegativeVersion() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + Assertions.assertThrows(IllegalArgumentException.class, () -> cfg.withVersion("version-key", -1, null)); + } + + @Test + void testWithTooNewVersion() { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()) + .withVersion(1); + + Assertions.assertThrows(IllegalStateException.class, () -> cfg.load(new StringReader("version: 10\n"))); + } + + @Test + void testWithUpToDateVersion() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()) + .withVersion(2); + + assertFalse(cfg.upgradeOldConfigVersion()); + } + + @Test + void testWithKeyWithoutConfigEntry() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + + cfg.set("test-key", "foo"); + cfg.load(); + + assertNull(cfg.get("test-key")); + + cfg.set("test-key", "foo"); + assertEquals("foo", cfg.get("test-key")); + + cfg.save(); + cfg.load(); + + assertEquals("foo", cfg.get("test-key")); + assertEquals(1, cfg.getKeys("").size()); + } + + @Test + void testDefaultValueAppliedAfterLoadNullValue() throws IOException { + SongodaYamlConfig cfg = new SongodaYamlConfig(this.cfg.toFile()); + ConfigEntry entry = new ConfigEntry(cfg, "key", "value"); + + cfg.init(); + + assertEquals("value", entry.get()); + } + + private Path createTmpFile() throws IOException { + Path path = Files.createTempFile("SongodaYamlConfigTest", "yml"); + File file = path.toFile(); + file.deleteOnExit(); + + return path; + } +}