diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..14f43d4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,57 @@
+# Changelog
+
+## v2.1.0 (SNAPSHOT)
+
+### Added
+- Added support for serialization of sealed classes and interfaces.
+ - Added `discriminator` property to the `@KtConfig` annotation for handling sealed hierarchies.
+- Added new methods to `KtConfigLoader` for easier file handling:
+ - `loadAndSave`: Loads a file and immediately saves it back.
+ - `loadAndSaveIfNotExists`: Loads a file and saves default values if the file doesn't exist.
+ - `saveIfNotExists`: Saves the configuration only if the file does not already exist.
+- Added `FormattedColorSerializer#isSupportedAlpha` property to detect Minecraft version support for an alpha channel in colors.
+
+### Changed
+- Improved the KSP code generator to use explicit imports instead of fully qualified names in generated loader classes.
+ - This results in cleaner and more readable generated code.
+ -
+ Example
+
+ ```kotlin
+ // Target
+ @KtConfig
+ data class ExampleConfig(
+ val string: String,
+ val list: List,
+ )
+
+ // Before
+ private val ListOfString: Serializer> =
+ dev.s7a.ktconfig.serializer.ListSerializer(dev.s7a.ktconfig.serializer.StringSerializer)
+
+ override fun load(configuration: YamlConfiguration, parentPath: String): ExampleConfig = ExampleConfig(
+ dev.s7a.ktconfig.serializer.StringSerializer.getOrThrow(configuration, "${parentPath}string"),
+ ListOfString.getOrThrow(configuration, "${parentPath}list"),
+ )
+
+ // After
+ private val ListOfString: Serializer> = ListSerializer(StringSerializer)
+
+ override fun load(configuration: YamlConfiguration, parentPath: String): ExampleConfig = ExampleConfig(
+ StringSerializer.getOrThrow(configuration, "${parentPath}string"),
+ ListOfString.getOrThrow(configuration, "${parentPath}list"),
+ )
+ ```
+
+- Deprecated `@PathName` and replaced it with `@SerialName` for better consistency.
+ - `@PathName` is scheduled to be removed in v2.4.0.
+- Fixed `FormattedColorSerializer` to ignore alpha values of 255 (fully opaque) when encoding colors, treating them as if no alpha channel is specified.
+
+### Fixed
+- Fixed KSP loader generation to respect nullability when the original type is a nullable typealias (previously, `Nullable` could be ignored and a non-null loader would be generated).
+- Fixed `FormattedColorSerializer` to ignore an alpha channel when encoding colors on Minecraft versions that don't support alpha transparency.
+- Fixed `BigInteger`, `BigDecimal` unsupported exception.
+
+## v2.0.0
+
+Initial release.
diff --git a/DEPRECATION.md b/DEPRECATION.md
new file mode 100644
index 0000000..7ff7427
--- /dev/null
+++ b/DEPRECATION.md
@@ -0,0 +1,32 @@
+# Deprecation List
+
+This document lists features and APIs that have been deprecated in `ktConfig`.
+
+## Annotations
+
+### @PathName
+
+- **Deprecated in**: v2.1.0
+- **Scheduled for removal**: v2.4.0
+- **Replacement**: `dev.s7a.ktconfig.SerialName`
+
+`@PathName` was used to specify a custom YAML path name for a property. It is being replaced by `@SerialName` to provide a unified annotation for both property paths and sealed class discriminators.
+
+#### Example Migration
+
+```kotlin
+// ❌ Deprecated (Compilation Error in v2.1.0+)
+@KtConfig
+data class Config(
+ @PathName("server-port")
+ val port: Int
+)
+
+// ✅ Recommended
+@KtConfig
+data class Config(
+ @SerialName("server-port")
+ val port: Int
+)
+```
+
diff --git a/README.md b/README.md
index 8afe090..6c2d24c 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,14 @@
# ktConfig v2
-Spigot configuration library for Kotlin using class annotations. The library generates configuration loaders at
-build-time, ensuring zero runtime overhead (except for YamlConfiguration operations).
+Spigot configuration library for Kotlin using class annotations.
+The library generates configuration loaders at build-time, ensuring zero runtime overhead (except for YamlConfiguration operations).
## ⚡ Features
- **Zero Runtime Overhead**: All configuration loaders are generated at build-time (KSP).
- **Type-Safe**: Fully typed configuration using Kotlin data classes.
-- **Wide Type Support**: Supports primitives, collections, Bukkit types, and more out of the box.
+- **Wide Type Support**: Supports primitives, collections, Bukkit types, and more.
+- **Sealed Classes and Interfaces Support**: Support for sealed classes and interfaces.
- **Rich Features**: Built-in support for comments and custom serializers.
- **Default Values**: Support for default values using Kotlin default values (e.g., `val count: Int = 0`).
@@ -22,12 +23,12 @@ plugins {
}
repositories {
- mavenCentral()
+ maven(url = "https://central.sonatype.com/repository/maven-snapshots/")
}
dependencies {
- implementation("dev.s7a:ktConfig:2.0.0")
- ksp("dev.s7a:ktConfig-ksp:2.0.0")
+ implementation("dev.s7a:ktConfig:2.1.0-SNAPSHOT")
+ ksp("dev.s7a:ktConfig-ksp:2.1.0-SNAPSHOT")
}
```
@@ -64,10 +65,13 @@ maxPlayers: 100
The loader class provides the following methods:
-- `load(File): T`
-- `loadFromString(String): T`
-- `save(T, File)`
-- `saveToString(T): String`
+- `load(File): T` - Loads configuration from the file.
+- `loadAndSave(File): T` - Loads configuration from the file and immediately saves it back.
+- `loadAndSaveIfNotExists(File): T` - Loads configuration from the file, or creates it with default values if it doesn't exist.
+- `loadFromString(String): T` - Loads configuration from a YAML string.
+- `save(File, T)` - Saves the configuration to the file.
+- `saveIfNotExists(File, T)` - Saves the configuration only if the file doesn't already exist.
+- `saveToString(T): String` - Serializes the configuration to a YAML string.
## 🚀 Usage
@@ -75,7 +79,7 @@ ktConfig provides various annotations to customize configuration behavior:
- `@KtConfig`: Marks a class as a configuration class. Required for code generation.
- `@Comment`: Adds comments to configuration headers or properties.
-- `@PathName`: Customizes the YAML path name for a property.
+- `@SerialName`: Customizes the YAML path name for a property.
- `@UseSerializer`: Specifies a custom serializer for a property.
### Adding Comments
@@ -93,12 +97,16 @@ data class AppConfig(
### Change the YAML Path Name
-You can customize the YAML path name using the `@PathName` annotation.
+You can customize the YAML path name using the `@SerialName` annotation.
+
+> [!WARNING]
+>
+> `@PathName` is deprecated since v2.1.0 and will be removed in v2.4.0.
```kotlin
@KtConfig
data class ServerConfig(
- @PathName("server-name")
+ @SerialName("server-name")
val serverName: String
)
```
@@ -228,6 +236,58 @@ data class CustomConfig(
)
```
+You can also use type aliases with custom serializers for cleaner code reuse:
+
+```kotlin
+typealias SerializableWrapper =
+ @UseSerializer(WrapperSerializer::class) Wrapper
+
+@KtConfig
+data class CustomConfig(
+ val data: SerializableWrapper
+)
+```
+
+### Sealed classes and interfaces
+
+- Use the `discriminator` property in `@KtConfig` to specify the YAML key name (default is `$`).
+- Use `@SerialName` on subclasses to define their identifier in YAML (default is the class full name).
+
+```kotlin
+@KtConfig(discriminator = "type")
+sealed interface AppConfig {
+ @KtConfig
+ @SerialName("message")
+ data class Message(
+ val content: String
+ ) : AppConfig
+
+ @KtConfig
+ @SerialName("broadcast")
+ data class Broadcast(
+ val content: String,
+ val delay: Int
+ ) : AppConfig
+}
+```
+
+#### YAML Representation
+
+Depending on the class being saved, the YAML will look like this:
+
+```yaml
+# For AppConfig.Message
+type: message
+content: "Hello World"
+```
+
+```yaml
+# For AppConfig.Broadcast
+type: broadcast
+content: "Attention!"
+delay: 20
+```
+
## 📦 Supported Types
ktConfig supports the following types:
@@ -287,6 +347,7 @@ ktConfig supports the following types:
- `java.time.Period`
- [Enum classes](https://kotlinlang.org/docs/enum-classes.html)
- [Inline value classes](https://kotlinlang.org/docs/inline-classes.html)
+- [Sealed classes and interfaces](https://kotlinlang.org/docs/sealed-classes.html)
### Formatted Types
diff --git a/build.gradle.kts b/build.gradle.kts
index a503ecd..5860b8a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -14,7 +14,7 @@ plugins {
}
group = "dev.s7a"
-version = "2.0.0"
+version = "2.1.0-SNAPSHOT"
allprojects {
apply(plugin = "kotlin")
diff --git a/example/build.gradle.kts b/example/build.gradle.kts
index 86912b0..75bf88a 100644
--- a/example/build.gradle.kts
+++ b/example/build.gradle.kts
@@ -20,7 +20,7 @@ repositories {
dependencies {
library(kotlin("stdlib"))
- compileOnly(libs.spigot8)
+ compileOnly(libs.spigot)
project.logger.lifecycle("Use Local Build: $useLocalBuild")
if (useLocalBuild) {
@@ -92,7 +92,27 @@ listOf(
.get()
.asFile.absolutePath,
)
- jarUrl.set(LaunchMinecraftServerTask.JarUrl.Paper(version))
+ // FIXME For some reason this doesn't work: 'kotlinx.serialization.KSerializer[] kotlinx.serialization.internal.GeneratedSerializer.typeParametersSerializers()'
+ // jarUrl.set(LaunchMinecraftServerTask.JarUrl.Paper(version))
+ jarUrl.set(
+ when (version) {
+ "1.21.11" -> {
+ LaunchMinecraftServerTask.JarUrl {
+ "https://fill-data.papermc.io/v1/objects/5be84d9fc43181a72d5fdee7e3167824d9667bfc97b1bf9721713f9a971481ca/paper-1.21.11-88.jar"
+ }
+ }
+
+ "1.8.8" -> {
+ LaunchMinecraftServerTask.JarUrl {
+ "https://fill-data.papermc.io/v1/objects/7ff6d2cec671ef0d95b3723b5c92890118fb882d73b7f8fa0a2cd31d97c55f86/paper-1.8.8-445.jar"
+ }
+ }
+
+ else -> {
+ error("Unknown Minecraft version: $version")
+ }
+ },
+ )
agreeEula.set(true)
}
}
diff --git a/example/src/main/kotlin/dev/s7a/example/ExamplePlugin.kt b/example/src/main/kotlin/dev/s7a/example/ExamplePlugin.kt
index c0667fc..5e04873 100644
--- a/example/src/main/kotlin/dev/s7a/example/ExamplePlugin.kt
+++ b/example/src/main/kotlin/dev/s7a/example/ExamplePlugin.kt
@@ -1,10 +1,15 @@
package dev.s7a.example
import dev.s7a.example.config.HasDefaultConfigLoader
+import dev.s7a.example.config.SealedTestConfig
+import dev.s7a.example.config.SealedTestConfigBLoader
+import dev.s7a.example.config.SealedTestConfigLoader
import dev.s7a.example.config.SerializerTestConfig
import dev.s7a.example.config.SerializerTestConfigLoader
import dev.s7a.example.type.CustomData
+import dev.s7a.ktconfig.type.FormattedColorSerializer
import org.bukkit.Bukkit
+import org.bukkit.Color
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.inventory.ItemStack
@@ -31,6 +36,8 @@ class ExamplePlugin : JavaPlugin() {
override fun onEnable() {
testSerializer()
testDefaultSerializer()
+ testSealedSerializer()
+ testFormattedColor()
server.shutdown()
}
@@ -77,6 +84,7 @@ class ExamplePlugin : JavaPlugin() {
formattedVector2 = Vector(Random.nextDouble(), Random.nextDouble(), Random.nextDouble()),
formattedVector3 = Vector(Random.nextDouble(), Random.nextDouble(), Random.nextDouble()),
formattedVector4 = Vector(Random.nextDouble(), Random.nextDouble(), Random.nextDouble()),
+ nullableFormattedVector = Vector(Random.nextDouble(), Random.nextDouble(), Random.nextDouble()),
customData = CustomData(Random.nextInt()),
array = Array(3) { UUID.randomUUID().toString() },
byteArray = ByteArray(3) { Random.nextInt(-128, 128).toByte() },
@@ -242,7 +250,7 @@ class ExamplePlugin : JavaPlugin() {
}
output.info()
- output.info("Check @PathName")
+ output.info("Check @SerialName")
if (lines.contains("path-name: ${expected.pathName}")) {
output.info("- Path name is overridden")
} else {
@@ -368,6 +376,11 @@ class ExamplePlugin : JavaPlugin() {
"formattedVector4: expected=${expected.formattedVector4}, actual=${actual.formattedVector4}",
)
}
+ if (expected.nullableFormattedVector != actual.nullableFormattedVector) {
+ output.error(
+ "nullableFormattedVector: expected=${expected.nullableFormattedVector}, actual=${actual.nullableFormattedVector}",
+ )
+ }
if (expected.customData != actual.customData) {
output.error(
"customData: expected=${expected.customData}, actual=${actual.customData}",
@@ -550,4 +563,134 @@ class ExamplePlugin : JavaPlugin() {
output.info("value: OK")
}
}
+
+ private fun testSealedSerializer() {
+ output.info("Test sealed serializer:")
+
+ // Use discriminator: '$' (ignore 'type')
+ listOf(
+ Pair(
+ """
+ $: dev.s7a.example.config.SealedTestConfig.A
+ a: text1
+ value: '5'
+ enum: TestA
+
+ """.trimIndent(),
+ SealedTestConfig.A("text1", 5, SealedTestConfig.A.Enum.TestA),
+ ),
+ Pair(
+ """
+ $: dev.s7a.example.config.SealedTestConfig.A
+ a: text1
+ value: '5'
+ enum: TestA
+
+ """.trimIndent(),
+ SealedTestConfig.A("text1", 5, SealedTestConfig.A.Enum.TestA),
+ ),
+ Pair(
+ """
+ $: b1
+ b1: text2
+ enum: TestB1
+
+ """.trimIndent(),
+ SealedTestConfig.B.B1("text2", SealedTestConfig.B.B1.Enum.TestB1),
+ ),
+ Pair(
+ """
+ $: dev.s7a.example.config.SealedTestConfig.B.B2
+ b2: text3
+
+ """.trimIndent(),
+ SealedTestConfig.B.B2("text3"),
+ ),
+ Pair(
+ """
+ $: dev.s7a.example.config.SealedTestConfig.C.C1
+ c1: text4
+
+ """.trimIndent(),
+ SealedTestConfig.C.C1("text4"),
+ ),
+ ).forEach { (yaml, expected) ->
+ val config = SealedTestConfigLoader.loadFromString(yaml)
+ if (config != expected) {
+ output.error("SealedTestConfig: expected=$expected, actual=$config")
+ } else {
+ output.info("SealedTestConfig: OK")
+ }
+
+ val actual = SealedTestConfigLoader.saveToString(config)
+ if (actual != yaml) {
+ output.error("SealedTestConfig: expected=$yaml, actual=$actual")
+ } else {
+ output.info("SealedTestConfig: OK")
+ }
+ }
+
+ // Use discriminator: 'type'
+ listOf(
+ Pair(
+ """
+ type: b1
+ b1: text2
+ enum: TestB1
+
+ """.trimIndent(),
+ SealedTestConfig.B.B1("text2", SealedTestConfig.B.B1.Enum.TestB1),
+ ),
+ Pair(
+ """
+ type: dev.s7a.example.config.SealedTestConfig.B.B2
+ b2: text3
+
+ """.trimIndent(),
+ SealedTestConfig.B.B2("text3"),
+ ),
+ ).forEach { (yaml, expected) ->
+ val config = SealedTestConfigBLoader.loadFromString(yaml)
+ if (config != expected) {
+ output.error("SealedTestConfigB: expected=$expected, actual=$config")
+ } else {
+ output.info("SealedTestConfigB: OK")
+ }
+
+ val actual = SealedTestConfigBLoader.saveToString(config)
+ if (actual != yaml) {
+ output.error("SealedTestConfigB: expected=$yaml, actual=$actual")
+ } else {
+ output.info("SealedTestConfigB: OK")
+ }
+ }
+ }
+
+ private fun testFormattedColor() {
+ val expected = Color.fromRGB(0x1F, 0x2E, 0x3D)
+ val actual = FormattedColorSerializer.decode("#1F2E3D")
+ if (actual != expected) {
+ output.error("FormattedColorSerializer: expected=$expected, actual=$actual")
+ } else {
+ output.info("FormattedColorSerializer: OK")
+ }
+
+ if (FormattedColorSerializer.isSupportedAlpha) {
+ // Check alpha
+ val expected = Color.fromARGB(0x1F, 0x2E, 0x3D, 0x4C)
+ val actual = FormattedColorSerializer.decode("#1F2E3D4C")
+ if (actual != expected) {
+ output.error("FormattedColorSerializer(alpha): expected=$expected, actual=$actual")
+ } else {
+ output.info("FormattedColorSerializer(alpha): OK")
+ }
+ } else {
+ // Check alpha support
+ if (Bukkit.getVersion().contains("1.8.8").not()) {
+ output.error("FormattedColorSerializer(alpha): isSupportedAlpha=false")
+ } else {
+ output.info("FormattedColorSerializer(alpha): Unsupported version")
+ }
+ }
+ }
}
diff --git a/example/src/main/kotlin/dev/s7a/example/config/SealedTestConfig.kt b/example/src/main/kotlin/dev/s7a/example/config/SealedTestConfig.kt
new file mode 100644
index 0000000..96e22ca
--- /dev/null
+++ b/example/src/main/kotlin/dev/s7a/example/config/SealedTestConfig.kt
@@ -0,0 +1,50 @@
+package dev.s7a.example.config
+
+import dev.s7a.ktconfig.KtConfig
+import dev.s7a.ktconfig.SerialName
+
+@KtConfig
+sealed interface SealedTestConfig {
+ @KtConfig
+ data class A(
+ val a: String,
+ val value: Int,
+ val enum: Enum,
+ ) : SealedTestConfig {
+ enum class Enum {
+ TestA,
+ }
+ }
+
+ @KtConfig(discriminator = "type")
+ sealed interface B : SealedTestConfig {
+ @KtConfig
+ @SerialName("b1")
+ data class B1(
+ val b1: String,
+ val enum: Enum,
+ ) : B {
+ enum class Enum {
+ TestB1,
+ }
+ }
+
+ @KtConfig
+ data class B2(
+ val b2: String,
+ ) : B
+ }
+
+ @KtConfig(discriminator = "") // -> $
+ sealed class C : SealedTestConfig {
+ @KtConfig
+ data class C1(
+ val c1: String,
+ ) : C()
+ }
+
+ @KtConfig
+ data class Ignored(
+ val ignored: String,
+ )
+}
diff --git a/example/src/main/kotlin/dev/s7a/example/config/SerializerTestConfig.kt b/example/src/main/kotlin/dev/s7a/example/config/SerializerTestConfig.kt
index 064fab1..0f97de2 100644
--- a/example/src/main/kotlin/dev/s7a/example/config/SerializerTestConfig.kt
+++ b/example/src/main/kotlin/dev/s7a/example/config/SerializerTestConfig.kt
@@ -3,7 +3,7 @@ package dev.s7a.example.config
import dev.s7a.example.type.CustomData
import dev.s7a.ktconfig.Comment
import dev.s7a.ktconfig.KtConfig
-import dev.s7a.ktconfig.PathName
+import dev.s7a.ktconfig.SerialName
import dev.s7a.ktconfig.UseSerializer
import dev.s7a.ktconfig.type.FormattedVector
import dev.s7a.ktconfig.type.FormattedVectorSerializer
@@ -67,6 +67,7 @@ data class SerializerTestConfig(
Vector,
val formattedVector3: FormattedVectorAlias,
val formattedVector4: FormattedVector2Alias,
+ val nullableFormattedVector: FormattedVector?,
val customData: CustomData,
val byteArray: ByteArray,
val charArray: CharArray,
@@ -108,7 +109,7 @@ data class SerializerTestConfig(
val nullableListMap: List