From ad55bff7f9275695e0f492811d07c902fdecde8d Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:18:56 +0100 Subject: [PATCH 1/5] feat: implement configuration migration system with builder pattern --- .../core/api/config/SpongeConfigClass.kt | 80 ++++++++ .../core/api/config/SpongeJsonConfigClass.kt | 9 +- .../core/api/config/SpongeYmlConfigClass.kt | 9 +- .../surfapi/core/api/config/SurfConfigApi.kt | 81 ++++---- .../api/config/manager/SpongeConfigManager.kt | 107 +++++++++- .../api/config/migration/ConfigMigration.kt | 41 ++++ .../migration/ConfigMigrationBuilder.kt | 190 ++++++++++++++++++ .../core/server/config/SurfConfigApiImpl.kt | 11 +- 8 files changed, 473 insertions(+), 55 deletions(-) create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigration.kt create mode 100644 surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt index 4b749949..c2e48d03 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt @@ -1,6 +1,9 @@ package dev.slne.surf.surfapi.core.api.config import dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager +import dev.slne.surf.surfapi.core.api.config.migration.ConfigMigration +import dev.slne.surf.surfapi.core.api.config.migration.ConfigMigrationBuilder +import org.spongepowered.configurate.ConfigurationNode import java.nio.file.Path /** @@ -12,10 +15,36 @@ import java.nio.file.Path * - persist changes via [save] * - reload the config from disk via [reloadFromFile] * - perform in-place mutations via [edit] + * - register schema migrations via [migration] * * The actual manager instance is provided by subclasses and typically created by * a central configuration API (e.g. [surfConfigApi]). * + * ## Versioned Migrations + * + * Migrations can be registered in the `init` block of companion objects: + * + * ```kotlin + * @ConfigSerializable + * data class MyConfig( + * var release: String = "1.0.0" + * ) { + * companion object : SpongeYmlConfigClass( + * MyConfig::class.java, + * Path("config/my-plugin"), + * "my-config.yml" + * ) { + * init { + * migration(1, RenameServerVersionMigration) + * migration(2) { node -> + * // inline migration + * node.node("old-field").raw(null) + * } + * } + * } + * } + * ``` + * * @param C the type of the configuration data object. * @param configClass the Java class of [C], used by underlying config frameworks * to create and map configuration instances. @@ -35,12 +64,63 @@ sealed class SpongeConfigClass( protected val fileName: String ) { + /** + * The migration builder that collects all registered migrations. + * + * Subclasses should use [migration] to register migrations rather than + * accessing this directly. + */ + protected val migrationBuilder = ConfigMigrationBuilder() + /** * The underlying configuration manager responsible for loading, saving, * and tracking the config instance of type [C]. */ abstract val manager: SpongeConfigManager + /** + * Registers a migration for the given target [version]. + * + * Migrations are applied in version order. For existing configs without a + * version field, **all** registered migrations will be applied the first time. + * + * @param version the target version this migration upgrades to (must be >= 0) + * @param migration the migration to apply + */ + protected fun migration(version: Int, migration: ConfigMigration) { + migrationBuilder.migration(version, migration) + } + + /** + * Registers an inline migration for the given target [version]. + * + * ```kotlin + * init { + * migration(1) { node -> + * node.node("old-key").raw(null) + * } + * } + * ``` + * + * @param version the target version this migration upgrades to (must be >= 0) + * @param migration the migration lambda + */ + protected inline fun migration(version: Int, crossinline migration: (ConfigurationNode) -> Unit) { + migrationBuilder.migration(version, ConfigMigration { node -> migration(node) }) + } + + /** + * Sets the path in the config file where the version number is stored. + * + * Defaults to `"config-version"`. Call this in the `init` block before + * any migration registrations if you need a custom key. + * + * @param path the path components to the version key + */ + protected fun versionKey(vararg path: Any) { + migrationBuilder.versionKey(*path) + } + /** * Persists the current configuration to disk. * diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt index 5813a91e..ca0b721d 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt @@ -6,7 +6,7 @@ import java.nio.file.Path * Convenience base class for JSON-backed Sponge configuration classes. * * This class wires the common configuration metadata ([configFolder], [fileName]) - * to a JSON-based [dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager] instance created by [surfConfigApi]. + * to a JSON-based [SpongeConfigManager][dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager] instance created by [surfConfigApi]. * * Typical usage is via a companion object on a `@ConfigSerializable` data class: * ```kotlin @@ -19,6 +19,9 @@ import java.nio.file.Path * Path("config/my-plugin"), * "my-config.json" * ) { + * init { + * migration(1, MyFirstMigration) + * } * } * } * ``` @@ -34,8 +37,8 @@ abstract class SpongeJsonConfigClass( * JSON-backed configuration manager for this config type. * * The manager is created using [SurfConfigApi.createSpongeJsonConfigManager] - * with [configClass], [configFolder] and [fileName]. + * with [configClass], [configFolder], [fileName] and [migrationBuilder]. */ override val manager = - surfConfigApi.createSpongeJsonConfigManager(configClass, configFolder, fileName) + surfConfigApi.createSpongeJsonConfigManager(configClass, configFolder, fileName, migrationBuilder) } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt index 466880a7..ffa2d272 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt @@ -6,7 +6,7 @@ import java.nio.file.Path * Convenience base class for YAML-backed Sponge configuration classes. * * This class wires the common configuration metadata ([configFolder], [fileName]) - * to a YAML-based [dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager] instance created by [surfConfigApi]. + * to a YAML-based [SpongeConfigManager][dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager] instance created by [surfConfigApi]. * * Typical usage is via a companion object on a `@ConfigSerializable` data class: * ```kotlin @@ -19,6 +19,9 @@ import java.nio.file.Path * Path("config/my-plugin"), * "my-config.yml" * ) { + * init { + * migration(1, MyFirstMigration) + * } * } * } * ``` @@ -34,8 +37,8 @@ abstract class SpongeYmlConfigClass( * YAML-backed configuration manager for this config type. * * The manager is created using [SurfConfigApi.createSpongeYmlConfigManager] - * with [configClass], [configFolder] and [fileName]. + * with [configClass], [configFolder], [fileName] and [migrationBuilder]. */ override val manager = - surfConfigApi.createSpongeYmlConfigManager(configClass, configFolder, fileName) + surfConfigApi.createSpongeYmlConfigManager(configClass, configFolder, fileName, migrationBuilder) } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt index 6efcdba5..256700e6 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SurfConfigApi.kt @@ -3,6 +3,7 @@ package dev.slne.surf.surfapi.core.api.config import dev.slne.surf.surfapi.core.api.config.manager.DazzlConfDeprecationMessageHolder import dev.slne.surf.surfapi.core.api.config.manager.PreferUsingSpongeConfigOverDazzlConf import dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager +import dev.slne.surf.surfapi.core.api.config.migration.ConfigMigrationBuilder import dev.slne.surf.surfapi.core.api.util.requiredService import java.nio.file.Path @@ -79,6 +80,24 @@ interface SurfConfigApi { configClass: Class, configFolder: Path, configFileName: @YamlConfigFileNamePattern String, + ): SpongeConfigManager = + createSpongeYmlConfigManager(configClass, configFolder, configFileName, ConfigMigrationBuilder()) + + /** + * Creates a Sponge YAML configuration manager. + * + * @param C The type of the configuration class. + * @param configClass The class of the configuration. + * @param configFolder The folder where the configuration file is stored. + * @param configFileName The name of the configuration file. Must follow the YAML file name pattern. + * @param migrations Optional migration builder with pre-registered migrations. + * @return An instance of [SpongeConfigManager] for the configuration class [C]. + */ + fun createSpongeYmlConfigManager( + configClass: Class, + configFolder: Path, + configFileName: @YamlConfigFileNamePattern String, + migrations: ConfigMigrationBuilder, ): SpongeConfigManager /** @@ -109,6 +128,24 @@ interface SurfConfigApi { configClass: Class, configFolder: Path, configFileName: @JsonConfigFileNamePattern String, + ): SpongeConfigManager = + createSpongeYmlConfigManager(configClass, configFolder, configFileName, ConfigMigrationBuilder()) + + /** + * Creates a Sponge JSON configuration manager. + * + * @param C The type of the configuration class. + * @param configClass The class of the configuration. + * @param configFolder The folder where the configuration file is stored. + * @param configFileName The name of the configuration file. Must follow the JSON file name pattern. + * @param migrations Optional migration builder with pre-registered migrations. + * @return An instance of [SpongeConfigManager] for the configuration class [C]. + */ + fun createSpongeJsonConfigManager( + configClass: Class, + configFolder: Path, + configFileName: @JsonConfigFileNamePattern String, + migrations: ConfigMigrationBuilder, ): SpongeConfigManager /** @@ -138,7 +175,7 @@ interface SurfConfigApi { */ fun getSpongeConfigManagerForConfig(configClass: Class): SpongeConfigManager - companion object: SurfConfigApi by surfConfigApi { + companion object : SurfConfigApi by surfConfigApi { /** * Retrieves the singleton instance of [SurfConfigApi]. */ @@ -153,11 +190,6 @@ val surfConfigApi = requiredService() /** * Creates a DazzlConf configuration using a reified type. - * - * @param C The type of the configuration class. - * @param configFolder The folder where the configuration file is stored. - * @param configFileName The name of the configuration file. Must follow the YAML file name pattern. - * @return An instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) @@ -169,9 +201,6 @@ inline fun SurfConfigApi.createDazzlConfig( /** * Retrieves a DazzlConf configuration using a reified type. - * - * @param C The type of the configuration class. - * @return An instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) @@ -180,9 +209,6 @@ inline fun SurfConfigApi.getDazzlConfig() = getDazzlConfig(C::class. /** * Reloads a DazzlConf configuration using a reified type. - * - * @param C The type of the configuration class. - * @return The reloaded instance of the configuration class [C]. */ @PreferUsingSpongeConfigOverDazzlConf @Deprecated(message = DazzlConfDeprecationMessageHolder.MESSAGE, level = DeprecationLevel.ERROR) @@ -191,11 +217,6 @@ inline fun SurfConfigApi.reloadDazzlConfig() = reloadDazzlConfig(C:: /** * Creates a Sponge YAML configuration using a reified type. - * - * @param C The type of the configuration class. - * @param configFolder The folder where the configuration file is stored. - * @param configFileName The name of the configuration file. Must follow the YAML file name pattern. - * @return An instance of the configuration class [C]. */ inline fun SurfConfigApi.createSpongeYmlConfig( configFolder: Path, @@ -204,24 +225,16 @@ inline fun SurfConfigApi.createSpongeYmlConfig( /** * Creates a Sponge YAML configuration manager using a reified type. - * - * @param C The type of the configuration class. - * @param configFolder The folder where the configuration file is stored. - * @param configFileName The name of the configuration file. Must follow the YAML file name pattern. - * @return An instance of [SpongeConfigManager] for the configuration class [C]. */ +@JvmOverloads inline fun SurfConfigApi.createSpongeYmlConfigManager( configFolder: Path, configFileName: @YamlConfigFileNamePattern String, -) = createSpongeYmlConfigManager(C::class.java, configFolder, configFileName) + migrations: ConfigMigrationBuilder = ConfigMigrationBuilder(), +) = createSpongeYmlConfigManager(C::class.java, configFolder, configFileName, migrations) /** * Creates a Sponge JSON configuration using a reified type. - * - * @param C The type of the configuration class. - * @param configFolder The folder where the configuration file is stored. - * @param configFileName The name of the configuration file. Must follow the JSON file name pattern. - * @return An instance of the configuration class [C]. */ inline fun SurfConfigApi.createSpongeJsonConfig( configFolder: Path, @@ -230,21 +243,15 @@ inline fun SurfConfigApi.createSpongeJsonConfig( /** * Creates a Sponge JSON configuration manager using a reified type. - * - * @param C The type of the configuration class. - * @param configFolder The folder where the configuration file is stored. - * @param configFileName The name of the configuration file. Must follow the JSON file name pattern. - * @return An instance of [SpongeConfigManager] for the configuration class [C]. */ +@JvmOverloads inline fun SurfConfigApi.createSpongeJsonConfigManager( configFolder: Path, configFileName: @JsonConfigFileNamePattern String, -) = createSpongeJsonConfigManager(C::class.java, configFolder, configFileName) + migrations: ConfigMigrationBuilder = ConfigMigrationBuilder(), +) = createSpongeJsonConfigManager(C::class.java, configFolder, configFileName, migrations) /** * Retrieves a Sponge configuration using a reified type. - * - * @param C The type of the configuration class. - * @return An instance of the configuration class [C]. */ inline fun SurfConfigApi.getSpongeConfig() = getSpongeConfig(C::class.java) \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt index f5c9ae9a..767ef045 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager.kt @@ -2,6 +2,10 @@ package dev.slne.surf.surfapi.core.api.config.manager import dev.slne.surf.surfapi.core.api.config.JsonConfigFileNamePattern import dev.slne.surf.surfapi.core.api.config.YamlConfigFileNamePattern +import dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager.Companion.json +import dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager.Companion.yaml +import dev.slne.surf.surfapi.core.api.config.migration.ConfigMigration +import dev.slne.surf.surfapi.core.api.config.migration.ConfigMigrationBuilder import dev.slne.surf.surfapi.core.api.config.serializer.SpongeConfigSerializers import dev.slne.surf.surfapi.core.api.util.logger import org.spongepowered.configurate.ConfigurateException @@ -21,6 +25,10 @@ import java.nio.file.Path * Manages configurations using Sponge's Configurate library, including loading, saving, and reloading configurations. * Supports multiple formats, including YAML and JSON. * + * Optionally supports **versioned migrations** via [ConfigMigrationBuilder]. When migrations are + * registered (either via the builder overloads of [yaml]/[json] or after construction via + * [addMigration]), they are applied automatically on first load and on every [reloadFromFile]. + * * @param C The type of the configuration class. * @property config The current configuration instance. */ @@ -28,7 +36,8 @@ class SpongeConfigManager private constructor( private val configClass: Class, @JvmField @field:Volatile var config: C, private val loader: ConfigurationLoader, - private val node: ConfigurationNode + private val node: ConfigurationNode, + private val migrationBuilder: ConfigMigrationBuilder ) { /** @@ -52,12 +61,18 @@ class SpongeConfigManager private constructor( /** * Reloads the configuration from the file. If loading fails, the current configuration remains unchanged. * + * Migrations are applied automatically if any are registered. + * * @return The reloaded configuration instance. * @throws UncheckedIOException if an I/O error occurs during reload. */ fun reloadFromFile(): C { try { val reloadedNode: ConfigurationNode = loader.load() + + // Apply migrations before deserializing + applyMigrations(reloadedNode) + val reloadedConfig = reloadedNode.get(configClass) if (reloadedConfig == null) { @@ -93,6 +108,59 @@ class SpongeConfigManager private constructor( } } + /** + * Registers a migration for the given target version. + * + * This can be called after construction to add migrations dynamically. + * Note: Migrations added after initial load will only take effect on the next [reloadFromFile]. + * + * @param version the target version this migration upgrades to (must be >= 0) + * @param migration the migration to apply + * @return this manager for chaining + */ + fun addMigration(version: Int, migration: ConfigMigration): SpongeConfigManager { + migrationBuilder.migration(version, migration) + return this + } + + /** + * Registers an inline migration for the given target version. + * + * @param version the target version this migration upgrades to (must be >= 0) + * @param migration the migration lambda + * @return this manager for chaining + */ + inline fun addMigration(version: Int, crossinline migration: (ConfigurationNode) -> Unit): SpongeConfigManager { + return addMigration(version, ConfigMigration { node -> migration(node) }) + } + + /** + * Returns the [ConfigMigrationBuilder] used by this manager. + * + * Can be used to inspect registered migrations or customize the version key. + */ + fun migrations(): ConfigMigrationBuilder = migrationBuilder + + /** + * Applies pending migrations to the given node and saves if any were applied. + */ + private fun applyMigrations(node: ConfigurationNode) { + if (!migrationBuilder.hasMigrations()) return + + try { + val result = migrationBuilder.migrate(node) + if (result.migrated) { + // Save the migrated node immediately so the version is persisted + loader.save(node) + } + } catch (e: ConfigurateException) { + log.atSevere() + .withCause(e) + .log("Failed to apply config migrations") + throw e + } + } + companion object { private val log = logger() @@ -104,18 +172,22 @@ class SpongeConfigManager private constructor( * @param configClass The class of the configuration. * @param configFolder The folder where the configuration file is stored. * @param configFileName The name of the configuration file. Must match the YAML file name pattern. + * @param migrations Optional migration builder with pre-registered migrations. * @return A new instance of [SpongeConfigManager]. */ + @JvmOverloads fun yaml( configClass: Class, configFolder: Path, - configFileName: @YamlConfigFileNamePattern String + configFileName: @YamlConfigFileNamePattern String, + migrations: ConfigMigrationBuilder = ConfigMigrationBuilder() ): SpongeConfigManager = buildConfigManager( "https://yamlchecker.com/", YamlConfigurationLoader.builder().nodeStyle(NodeStyle.BLOCK), configClass, configFolder, - configFileName + configFileName, + migrations ) /** @@ -125,18 +197,22 @@ class SpongeConfigManager private constructor( * @param configClass The class of the configuration. * @param configFolder The folder where the configuration file is stored. * @param configFileName The name of the configuration file. Must match the JSON file name pattern. + * @param migrations Optional migration builder with pre-registered migrations. * @return A new instance of [SpongeConfigManager]. */ + @JvmOverloads fun json( configClass: Class, configFolder: Path, - configFileName: @JsonConfigFileNamePattern String + configFileName: @JsonConfigFileNamePattern String, + migrations: ConfigMigrationBuilder = ConfigMigrationBuilder() ): SpongeConfigManager = buildConfigManager( "https://jsonlint.com/", JacksonConfigurationLoader.builder(), configClass, configFolder, - configFileName + configFileName, + migrations ) @@ -144,7 +220,9 @@ class SpongeConfigManager private constructor( verifyToolUrl: String, builder: AbstractConfigurationLoader.Builder, configClass: Class, - configFolder: Path, configFileName: String + configFolder: Path, + configFileName: String, + migrations: ConfigMigrationBuilder ): SpongeConfigManager { val loader = builder.path(configFolder.resolve(configFileName)) .defaultOptions { @@ -155,13 +233,26 @@ class SpongeConfigManager private constructor( try { val node: ScopedConfigurationNode<*> = loader.load() + + // Apply migrations before deserializing into the config class. + // This handles both: + // - Existing configs without a version field (version = -1, all migrations run) + // - Configs with an older version (only newer migrations run) + if (migrations.hasMigrations()) { + val result = migrations.migrate(node) + if (result.migrated) { + loader.save(node) // persist migration + version field + } + } + val config = node.get(configClass) ?: throw LoadConfigException("Config is null after load") - loader.save(node) + // Re-save to ensure defaults are written node.set(configClass, config) + loader.save(node) - return SpongeConfigManager(configClass, config, loader, node) + return SpongeConfigManager(configClass, config, loader, node, migrations) } catch (e: SerializationException) { log.atSevere() .withCause(e) diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigration.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigration.kt new file mode 100644 index 00000000..b46650d0 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigration.kt @@ -0,0 +1,41 @@ +package dev.slne.surf.surfapi.core.api.config.migration + +import org.spongepowered.configurate.ConfigurationNode + +/** + * Represents a single configuration migration step. + * + * Each migration is responsible for transforming the raw [ConfigurationNode] tree + * from one schema version to the next. Migrations are applied in version order + * (lowest to highest) and only when the config's current version is lower than + * the migration's target version. + * + * Implementations should be stateless and side-effect-free (aside from mutating + * the provided node tree). + * + * ## Example + * + * ```kotlin + * object RenameServerVersionMigration : ConfigMigration { + * override fun migrate(node: ConfigurationNode) { + * val old = node.node("server", "version") + * if (!old.virtual()) { + * node.node("server", "release").set(old.raw()) + * old.raw(null) + * } + * } + * } + * ``` + */ +fun interface ConfigMigration { + + /** + * Applies this migration to the given configuration node tree. + * + * The [node] is the **root** node of the configuration. Implementations + * can freely read, modify, move, or remove any child nodes. + * + * @param node the root configuration node to transform + */ + fun migrate(node: ConfigurationNode) +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt new file mode 100644 index 00000000..608e49f2 --- /dev/null +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt @@ -0,0 +1,190 @@ +package dev.slne.surf.surfapi.core.api.config.migration + +import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf +import it.unimi.dsi.fastutil.ints.Int2ObjectRBTreeMap +import it.unimi.dsi.fastutil.objects.ObjectList +import org.spongepowered.configurate.ConfigurateException +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.NodePath +import org.spongepowered.configurate.transformation.ConfigurationTransformation + +/** + * Collects [ConfigMigration] instances keyed by target version and builds + * a Configurate [ConfigurationTransformation.Versioned] from them. + * + * The version key defaults to `"version"` at the root of the config file. + * This can be customized via [versionKey]. + * + * ## Backwards Compatibility + * + * Existing configs that were created **before** versioning was introduced will + * have no version field. Configurate treats missing version fields as version `-1` + * (`VERSION_UNKNOWN`), which means **all** registered migrations will be applied + * in order the first time such a config is loaded. The version field is then + * written automatically after migration. + * + * ## Usage + * + * ```kotlin + * val builder = ConfigMigrationBuilder() + * builder.migration(1, MyFirstMigration) + * builder.migration(2, MySecondMigration) + * + * // Apply to a loaded node: + * builder.migrate(node) // applies pending migrations & updates version field + * ``` + */ +class ConfigMigrationBuilder { + + private val migrations = Int2ObjectRBTreeMap>() + private var versionKeyPath: Array = arrayOf(DEFAULT_VERSION_KEY) + + /** + * Registers a [migration] for the given target [version]. + * + * When applied, this migration will run if the config's current version is + * **less than** [version]. Multiple migrations can be registered for the same + * version; they will run in registration order. + * + * @param version the target version this migration upgrades **to** (must be >= 0) + * @param migration the migration to apply + * @return this builder for chaining + * @throws IllegalArgumentException if [version] is negative + */ + fun migration(version: Int, migration: ConfigMigration): ConfigMigrationBuilder { + require(version >= 0) { "Migration version must be >= 0, was $version" } + migrations.computeIfAbsent(version) { mutableObjectListOf() }.add(migration) + return this + } + + /** + * Registers an inline migration for the given target [version]. + * + * Convenience overload that accepts a lambda instead of a [ConfigMigration] instance. + * + * ```kotlin + * migration(1) { node -> + * node.node("old-key").raw(null) + * } + * ``` + * + * @param version the target version this migration upgrades **to** (must be >= 0) + * @param migration the migration lambda to apply + * @return this builder for chaining + */ + inline fun migration(version: Int, crossinline migration: (ConfigurationNode) -> Unit): ConfigMigrationBuilder { + return migration(version, ConfigMigration { node -> migration(node) }) + } + + /** + * Sets the path in the config file where the version number is stored. + * + * Defaults to `"config-version"` (a single root-level key). + * + * @param path the path components to the version key + * @return this builder for chaining + */ + fun versionKey(vararg path: Any): ConfigMigrationBuilder { + versionKeyPath = path + return this + } + + /** + * Returns `true` if at least one migration has been registered. + */ + fun hasMigrations(): Boolean = migrations.isNotEmpty() + + /** + * Returns the highest registered version, or `-1` if no migrations are registered. + */ + fun latestVersion(): Int = if (migrations.isEmpty()) -1 else migrations.lastIntKey() + + /** + * Builds a Configurate [ConfigurationTransformation.Versioned] from the + * registered migrations. + * + * @return the versioned transformation, or `null` if no migrations are registered + */ + fun buildTransformation(): ConfigurationTransformation.Versioned? { + if (migrations.isEmpty()) return null + + val builder = ConfigurationTransformation.versionedBuilder() + .versionKey(*versionKeyPath) + + migrations.int2ObjectEntrySet().iterator().forEachRemaining { entry -> + val version = entry.intKey + val migrationList = entry.value + + val transformation = ConfigurationTransformation.builder().apply { + addAction(NodePath.path()) { _, node -> + for (migration in migrationList) { + migration.migrate(node) + } + null + } + }.build() + + builder.addVersion(version, transformation) + } + + return builder.build() + } + + /** + * Applies all pending migrations to the given [node]. + * + * This will: + * 1. Read the current version from the node (defaulting to `-1` if absent) + * 2. Apply all migrations with a version greater than the current version, in order + * 3. Update the version field in the node to the latest version + * + * @param node the root configuration node to migrate + * @return the version the node was migrated from, or `-1` if unversioned + * @throws ConfigurateException if a migration fails + */ + @Throws(ConfigurateException::class) + fun migrate(node: ConfigurationNode): MigrationResult { + val transformation = buildTransformation() + ?: return MigrationResult(fromVersion = -1, toVersion = -1, migrated = false) + + val startVersion = transformation.version(node) + transformation.apply(node) + val endVersion = transformation.version(node) + + val migrated = startVersion != endVersion + + if (migrated) { + log.atInfo() + .log("Migrated config from version %d to %d", startVersion, endVersion) + } + + return MigrationResult( + fromVersion = startVersion, + toVersion = endVersion, + migrated = migrated + ) + } + + companion object { + private val log = logger() + + /** + * Default key used to store the config schema version. + */ + const val DEFAULT_VERSION_KEY = "version" + } +} + +/** + * Result of a migration run. + * + * @property fromVersion the version before migration (`-1` if unversioned) + * @property toVersion the version after migration + * @property migrated `true` if any migrations were actually applied + */ +data class MigrationResult( + val fromVersion: Int, + val toVersion: Int, + val migrated: Boolean +) \ No newline at end of file diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/SurfConfigApiImpl.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/SurfConfigApiImpl.kt index deec1cf5..6f02cc0b 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/SurfConfigApiImpl.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/surfapi/core/server/config/SurfConfigApiImpl.kt @@ -6,6 +6,7 @@ import dev.slne.surf.surfapi.core.api.config.SurfConfigApi import dev.slne.surf.surfapi.core.api.config.YamlConfigFileNamePattern import dev.slne.surf.surfapi.core.api.config.manager.PreferUsingSpongeConfigOverDazzlConf import dev.slne.surf.surfapi.core.api.config.manager.SpongeConfigManager +import dev.slne.surf.surfapi.core.api.config.migration.ConfigMigrationBuilder import dev.slne.surf.surfapi.core.api.util.checkInstantiationByServiceLoader import java.nio.file.Path @@ -37,9 +38,10 @@ class SurfConfigApiImpl : SurfConfigApi { override fun createSpongeYmlConfigManager( configClass: Class, configFolder: Path, - configFileName: @YamlConfigFileNamePattern String + configFileName: @YamlConfigFileNamePattern String, + migrations: ConfigMigrationBuilder ): SpongeConfigManager { - val manager = SpongeConfigManager.yaml(configClass, configFolder, configFileName) + val manager = SpongeConfigManager.yaml(configClass, configFolder, configFileName, migrations) SpongeConfigTracker.registerConfig(configClass, manager) return manager @@ -48,9 +50,10 @@ class SurfConfigApiImpl : SurfConfigApi { override fun createSpongeJsonConfigManager( configClass: Class, configFolder: Path, - configFileName: @JsonConfigFileNamePattern String + configFileName: @JsonConfigFileNamePattern String, + migrations: ConfigMigrationBuilder ): SpongeConfigManager { - val manager = SpongeConfigManager.json(configClass, configFolder, configFileName) + val manager = SpongeConfigManager.json(configClass, configFolder, configFileName, migrations) SpongeConfigTracker.registerConfig(configClass, manager) return manager From eb2a0296fd79ae888b12ddb66284d03e481657fa Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:21:11 +0100 Subject: [PATCH 2/5] chore: dumb abi --- .../api/surf-api-core-api.api | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api index 23feb198..fa35cd2c 100644 --- a/surf-api-core/surf-api-core-api/api/surf-api-core-api.api +++ b/surf-api-core/surf-api-core-api/api/surf-api-core-api.api @@ -152,9 +152,13 @@ public abstract class dev/slne/surf/surfapi/core/api/config/SpongeConfigClass { protected final fun getConfigFolder ()Ljava/nio/file/Path; protected final fun getFileName ()Ljava/lang/String; public abstract fun getManager ()Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + protected final fun getMigrationBuilder ()Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder; public final fun init ()V + protected final fun migration (ILdev/slne/surf/surfapi/core/api/config/migration/ConfigMigration;)V + protected final fun migration (ILkotlin/jvm/functions/Function1;)V public final fun reloadFromFile ()Ljava/lang/Object; public final fun save ()V + protected final fun versionKey ([Ljava/lang/Object;)V } public abstract class dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass : dev/slne/surf/surfapi/core/api/config/SpongeConfigClass { @@ -171,9 +175,11 @@ public abstract interface class dev/slne/surf/surfapi/core/api/config/SurfConfig public static final field Companion Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi$Companion; public abstract fun createDazzlConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; public abstract fun createSpongeJsonConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; - public abstract fun createSpongeJsonConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public fun createSpongeJsonConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public abstract fun createSpongeJsonConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; public abstract fun createSpongeYmlConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; - public abstract fun createSpongeYmlConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public fun createSpongeYmlConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public abstract fun createSpongeYmlConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; public abstract fun getDazzlConfig (Ljava/lang/Class;)Ljava/lang/Object; public abstract fun getSpongeConfig (Ljava/lang/Class;)Ljava/lang/Object; public abstract fun getSpongeConfigManagerForConfig (Ljava/lang/Class;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; @@ -185,8 +191,10 @@ public final class dev/slne/surf/surfapi/core/api/config/SurfConfigApi$Companion public fun createDazzlConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; public fun createSpongeJsonConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; public fun createSpongeJsonConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public fun createSpongeJsonConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; public fun createSpongeYmlConfig (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ljava/lang/Object; public fun createSpongeYmlConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public fun createSpongeYmlConfigManager (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; public fun getDazzlConfig (Ljava/lang/Class;)Ljava/lang/Object; public final fun getInstance ()Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi; public fun getSpongeConfig (Ljava/lang/Class;)Ljava/lang/Object; @@ -195,7 +203,14 @@ public final class dev/slne/surf/surfapi/core/api/config/SurfConfigApi$Companion public fun reloadSpongeConfig (Ljava/lang/Class;)Ljava/lang/Object; } +public final class dev/slne/surf/surfapi/core/api/config/SurfConfigApi$DefaultImpls { + public static fun createSpongeJsonConfigManager (Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi;Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public static fun createSpongeYmlConfigManager (Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi;Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; +} + public final class dev/slne/surf/surfapi/core/api/config/SurfConfigApiKt { + public static final synthetic fun createSpongeJsonConfigManager (Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public static final synthetic fun createSpongeYmlConfigManager (Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; public static final fun getSurfConfigApi ()Ldev/slne/surf/surfapi/core/api/config/SurfConfigApi; } @@ -243,16 +258,58 @@ public final class dev/slne/surf/surfapi/core/api/config/manager/SerializationCo public final class dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager { public static final field Companion Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager$Companion; public field config Ljava/lang/Object; - public synthetic fun (Ljava/lang/Class;Ljava/lang/Object;Lorg/spongepowered/configurate/loader/ConfigurationLoader;Lorg/spongepowered/configurate/ConfigurationNode;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/Class;Ljava/lang/Object;Lorg/spongepowered/configurate/loader/ConfigurationLoader;Lorg/spongepowered/configurate/ConfigurationNode;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addMigration (ILdev/slne/surf/surfapi/core/api/config/migration/ConfigMigration;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public final fun addMigration (ILkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; public final fun edit (ZLkotlin/jvm/functions/Function1;)V public static synthetic fun edit$default (Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V + public final fun migrations ()Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder; public final fun reloadFromFile ()Ljava/lang/Object; public final fun save ()V } public final class dev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager$Companion { public final fun json (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public final fun json (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public static synthetic fun json$default (Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager$Companion;Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; public final fun yaml (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public final fun yaml (Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; + public static synthetic fun yaml$default (Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager$Companion;Ljava/lang/Class;Ljava/nio/file/Path;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/config/manager/SpongeConfigManager; +} + +public abstract interface class dev/slne/surf/surfapi/core/api/config/migration/ConfigMigration { + public abstract fun migrate (Lorg/spongepowered/configurate/ConfigurationNode;)V +} + +public final class dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder { + public static final field Companion Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder$Companion; + public static final field DEFAULT_VERSION_KEY Ljava/lang/String; + public fun ()V + public final fun buildTransformation ()Lorg/spongepowered/configurate/transformation/ConfigurationTransformation$Versioned; + public final fun hasMigrations ()Z + public final fun latestVersion ()I + public final fun migrate (Lorg/spongepowered/configurate/ConfigurationNode;)Ldev/slne/surf/surfapi/core/api/config/migration/MigrationResult; + public final fun migration (ILdev/slne/surf/surfapi/core/api/config/migration/ConfigMigration;)Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder; + public final fun migration (ILkotlin/jvm/functions/Function1;)Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder; + public final fun versionKey ([Ljava/lang/Object;)Ldev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder; +} + +public final class dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder$Companion { +} + +public final class dev/slne/surf/surfapi/core/api/config/migration/MigrationResult { + public fun (IIZ)V + public final fun component1 ()I + public final fun component2 ()I + public final fun component3 ()Z + public final fun copy (IIZ)Ldev/slne/surf/surfapi/core/api/config/migration/MigrationResult; + public static synthetic fun copy$default (Ldev/slne/surf/surfapi/core/api/config/migration/MigrationResult;IIZILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/config/migration/MigrationResult; + public fun equals (Ljava/lang/Object;)Z + public final fun getFromVersion ()I + public final fun getMigrated ()Z + public final fun getToVersion ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class dev/slne/surf/surfapi/core/api/config/serializer/DefaultDazzlConfSerializers { From 7159ebf5358143ecc43b2f593592cd862b5597ce Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 29 Mar 2026 11:58:45 +0200 Subject: [PATCH 3/5] feat: add configuration migration support with example config class --- .../surfapi/bukkit/test/BukkitPluginMain.kt | 3 + .../bukkit/test/config/MyPluginConfig.kt | 57 +++++++++++++++++++ .../core/api/config/SpongeConfigClass.kt | 4 +- .../core/api/config/SpongeJsonConfigClass.kt | 3 +- .../core/api/config/SpongeYmlConfigClass.kt | 3 +- .../migration/ConfigMigrationBuilder.kt | 4 +- 6 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/MyPluginConfig.kt diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt index bbdd323a..e70bd008 100644 --- a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt @@ -11,6 +11,7 @@ import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.TestInven import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.testInventoryViewDsl import dev.slne.surf.surfapi.bukkit.test.command.subcommands.reflection.Reflection import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig +import dev.slne.surf.surfapi.bukkit.test.config.MyPluginConfig import dev.slne.surf.surfapi.bukkit.test.listener.ChatListener import dev.slne.surf.surfapi.core.api.component.surfComponentApi @@ -31,6 +32,8 @@ class BukkitPluginMain : SuspendingJavaPlugin() { dialogTestCommand() Reflection::class.java.getClassLoader() // initialize Reflection + MyPluginConfig.init() + surfComponentApi.enable(this) } diff --git a/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/MyPluginConfig.kt b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/MyPluginConfig.kt new file mode 100644 index 00000000..1d9d0227 --- /dev/null +++ b/surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/MyPluginConfig.kt @@ -0,0 +1,57 @@ +package dev.slne.surf.surfapi.bukkit.test.config + +import dev.slne.surf.surfapi.bukkit.test.plugin +import dev.slne.surf.surfapi.core.api.config.SpongeYmlConfigClass +import dev.slne.surf.surfapi.core.api.config.migration.ConfigMigration +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +/** + * Example config class with migrations. + * Before migration: + * ```yaml + * server: + * version: "1-20-4" + * deprecated-field: "please-remove-me" + * max-players: 0 + * ``` + */ +@ConfigSerializable +data class MyPluginConfig( + var release: String = "1.0.0", + var maxPlayers: Int = 100 +) { + companion object : SpongeYmlConfigClass( + MyPluginConfig::class.java, + plugin.dataPath, + "migration-example-config.yml" + ) { + init { + migration(1, RenameServerVersionMigration) + migration(2, RemoveDeprecatedFieldMigration) + migration(3) { node -> + // inline migration: rename maxPlayers default + val mp = node.node("max-players") + if (!mp.virtual() && mp.getInt(0) == 0) { + mp.set(100) + } + } + } + } +} + +object RenameServerVersionMigration : ConfigMigration { + override fun migrate(node: ConfigurationNode) { + val old = node.node("server", "version") + if (!old.virtual()) { + node.node("server", "release").set(old.raw()) + old.raw(null) + } + } +} + +object RemoveDeprecatedFieldMigration : ConfigMigration { + override fun migrate(node: ConfigurationNode) { + node.node("deprecated-field").raw(null) + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt index c2e48d03..74e9c18e 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeConfigClass.kt @@ -172,5 +172,7 @@ sealed class SpongeConfigClass( * * This method is a no-op and can be safely called multiple times. */ - fun init() = Unit + fun init() { + manager + } } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt index ca0b721d..ce2f2d3f 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeJsonConfigClass.kt @@ -39,6 +39,7 @@ abstract class SpongeJsonConfigClass( * The manager is created using [SurfConfigApi.createSpongeJsonConfigManager] * with [configClass], [configFolder], [fileName] and [migrationBuilder]. */ - override val manager = + override val manager by lazy { surfConfigApi.createSpongeJsonConfigManager(configClass, configFolder, fileName, migrationBuilder) + } } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt index ffa2d272..99a41266 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/SpongeYmlConfigClass.kt @@ -39,6 +39,7 @@ abstract class SpongeYmlConfigClass( * The manager is created using [SurfConfigApi.createSpongeYmlConfigManager] * with [configClass], [configFolder], [fileName] and [migrationBuilder]. */ - override val manager = + override val manager by lazy { surfConfigApi.createSpongeYmlConfigManager(configClass, configFolder, fileName, migrationBuilder) + } } \ No newline at end of file diff --git a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt index 608e49f2..e765af1d 100644 --- a/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt +++ b/surf-api-core/surf-api-core-api/src/main/kotlin/dev/slne/surf/surfapi/core/api/config/migration/ConfigMigrationBuilder.kt @@ -13,7 +13,7 @@ import org.spongepowered.configurate.transformation.ConfigurationTransformation * Collects [ConfigMigration] instances keyed by target version and builds * a Configurate [ConfigurationTransformation.Versioned] from them. * - * The version key defaults to `"version"` at the root of the config file. + * The version key defaults to `"_config_version"` at the root of the config file. * This can be customized via [versionKey]. * * ## Backwards Compatibility @@ -172,7 +172,7 @@ class ConfigMigrationBuilder { /** * Default key used to store the config schema version. */ - const val DEFAULT_VERSION_KEY = "version" + const val DEFAULT_VERSION_KEY = "_config_version" } } From c137dde216398f5a9c05b8c6dddff0d51c8de13c Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:01:03 +0200 Subject: [PATCH 4/5] feat: update ABI checks to use Kotlin ABI commands --- .github/workflows/api-dump-version.yml | 6 +++--- .github/workflows/publish.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/api-dump-version.yml b/.github/workflows/api-dump-version.yml index 8d3a3aeb..0727ada8 100644 --- a/.github/workflows/api-dump-version.yml +++ b/.github/workflows/api-dump-version.yml @@ -33,7 +33,7 @@ jobs: id: check_api run: | set +e - ./gradlew checkLegacyAbi + ./gradlew checkKotlinAbi CHECK_EXIT=$? echo "check_exit=$CHECK_EXIT" >> $GITHUB_OUTPUT set -e @@ -46,7 +46,7 @@ jobs: echo "✅ No API changes detected." else echo "api_changed=true" >> $GITHUB_OUTPUT - echo "❌ API changes detected! To update the reference, run './gradlew updateLegacyAbi' locally and commit the changes." >&2 + echo "❌ API changes detected! To update the reference, run './gradlew updateKotlinAbi' locally and commit the changes." >&2 fi - name: Comment on PR if API changed @@ -61,7 +61,7 @@ jobs: This PR contains changes that modified the public API. To update the reference ABI dumps: ```bash - ./gradlew updateLegacyAbi + ./gradlew updateKotlinAbi git add **/api/** git commit -m "Update ABI reference" git push diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 50b66374..22f26fea 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,7 +40,7 @@ jobs: run: ./gradlew build shadowJar --parallel --no-scan - name: Check all modules with Gradle - run: ./gradlew check checkLegacyAbi --parallel --no-scan + run: ./gradlew check checkKotlinAbi --parallel --no-scan - name: Publish all modules to Maven run: ./gradlew publish --parallel --no-scan From 2b0491c5cad7dd4d1d2ab6448742fcfc64842d8a Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:02:54 +0200 Subject: [PATCH 5/5] chore: bump version to 1.21.11-2.72.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1e38f5e0..6d89919f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=1.21.11 group=dev.slne.surf -version=1.21.11-2.71.2 +version=1.21.11-2.72.0 relocationPrefix=dev.slne.surf.surfapi.libs snapshot=false