From 74e183fe63ba064c5a7d506f2d586efd59495d36 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 13:12:50 +0200 Subject: [PATCH 01/18] =?UTF-8?q?=E2=9C=A8=20feat(config):=20add=20new=20c?= =?UTF-8?q?onfiguration=20types=20for=20numeric=20and=20boolean=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce BelowZeroToEmpty annotation to treat negative numbers as empty - implement BooleanOrDefault type for optional boolean configuration - add DoubleOr and IntOr interfaces for optional numeric values with default/disabled states - create serializers for new configuration types to handle serialization and deserialization --- .../api/core/config/constraints/MaxNumber.kt | 38 +++ .../api/core/config/constraints/MinNumber.kt | 39 +++ .../core/config/constraints/PositiveNumber.kt | 38 +++ .../config/serializer/DurationSerializer.kt | 64 +++++ .../config/serializer/EnumValueSerializer.kt | 49 ++++ .../core/config/serializer/KeySerializer.kt | 34 +++ .../serializer/SpongeConfigSerializers.kt | 116 ++++++-- .../collection/map/FastutilMapSerializer.kt | 144 ++++++++++ .../collection/map/MapSerializer.kt | 258 ++++++++++++++++++ .../collection/map/ThrowExceptions.kt | 19 ++ .../serializer/collection/map/WriteKeyBack.kt | 22 ++ .../api/core/config/type/BooleanOrDefault.kt | 89 ++++++ .../core/config/type/DurationOrDisabled.kt | 95 +++++++ .../config/type/number/BelowZeroToEmpty.kt | 22 ++ .../api/core/config/type/number/DoubleOr.kt | 197 +++++++++++++ .../surf/api/core/config/type/number/IntOr.kt | 199 ++++++++++++++ .../api/paper/test/config/TestConfig2.java | 3 + .../bukkit/test/config/ModernTestConfig.kt | 12 +- 18 files changed, 1416 insertions(+), 22 deletions(-) create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/PositiveNumber.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/KeySerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/BooleanOrDefault.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/DoubleOr.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/IntOr.kt diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt new file mode 100644 index 000000000..1d61a2642 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt @@ -0,0 +1,38 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import java.lang.reflect.Type + +/** + * Validates that a numeric configuration value is less than or equal to [max]. + * + * `null` values are ignored, so this can also be used with nullable numeric fields. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class RateLimitConfig( + * @field:MaxNumber(100.0) + * val percentage: Double = 50.0 + * ) + * ``` + * + * @property max The inclusive maximum value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxNumber(val max: Double) { + companion object { + + /** + * Configurate constraint factory for [MaxNumber]. + */ + internal object Factory : Constraint.Factory { + override fun make(data: MaxNumber, type: Type): Constraint = { number -> + if (number != null && number.toDouble() > data.max) { + throw IllegalArgumentException("Number is too big: $number, expected <= ${data.max}") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt new file mode 100644 index 000000000..bccc8c4a8 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt @@ -0,0 +1,39 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import java.lang.reflect.Type + +/** + * Validates that a numeric configuration value is greater than or equal to [min]. + * + * `null` values are ignored, so this can also be used with nullable numeric fields. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class EconomyConfig( + * @field:MinNumber(0.0) + * val startingBalance: Double = 100.0 + * ) + * ``` + * + * @property min The inclusive minimum value. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinNumber(val min: Double) { + companion object { + + /** + * Configurate constraint factory for [MinNumber]. + */ + internal object Factory : Constraint.Factory { + override fun make(data: MinNumber, type: Type): Constraint = { number -> + if (number != null && number.toDouble() < data.min) { + throw IllegalArgumentException("Number is too small: $number, expected >= ${data.min}") + } + } + } + } +} + diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/PositiveNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/PositiveNumber.kt new file mode 100644 index 000000000..cad0081e0 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/PositiveNumber.kt @@ -0,0 +1,38 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Validates that a numeric configuration value is strictly positive. + * + * `null` values are ignored, so this can also be used with nullable numeric fields. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class ServerConfig( + * @field:PositiveNumber + * val maxPlayers: Int = 100 + * ) + * ``` + * + * Values less than or equal to zero are rejected. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class PositiveNumber { + companion object { + /** + * Configurate constraint factory for [PositiveNumber]. + */ + internal object Factory : Constraint.Factory { + override fun make(data: PositiveNumber, type: Type): Constraint = { number -> + if (number != null && number.toDouble() <= 0) { + throw SerializationException("Number is not positive: $number, expected > 0") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt new file mode 100644 index 000000000..9f73e597f --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt @@ -0,0 +1,64 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.math.BigDecimal +import java.util.function.Predicate +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +/** + * Configurate scalar serializer for Kotlin [Duration] values. + * + * Supported configuration formats: + * - `10s` + * - `5m` + * - `2h` + * - `1d` + * + * Serialized durations are written back as seconds. + */ +internal object DurationSerializer : ScalarSerializer.Annotated(Duration::class.java) { + private val DURATION_PATTERN = Regex("""^\s*(-?\d+(?:\.\d+)?)\s*([dhms])\s*$""", RegexOption.IGNORE_CASE) + + /** + * Parses a duration string into a Kotlin [Duration]. + */ + override fun deserialize(type: AnnotatedType, obj: Any): Duration { + val value = obj.toString() + val match = DURATION_PATTERN.matchEntire(value) + ?: throw SerializationException(Duration::class.java, "$obj($type) is not a duration") + + val amount = match.groupValues[1].toDoubleOrNull() + ?: throw SerializationException(Duration::class.java, "$obj($type) is not a duration") + + val unit = when (match.groupValues[2].lowercase()) { + "d" -> DurationUnit.DAYS + "h" -> DurationUnit.HOURS + "m" -> DurationUnit.MINUTES + "s" -> DurationUnit.SECONDS + else -> throw SerializationException(Duration::class.java, "$obj($type) is not a duration") + } + + return amount.toDuration(unit) + } + + /** + * Serializes a finite Kotlin [Duration] as a seconds-based duration string. + * + * @throws SerializationException if [item] is infinite. + */ + public override fun serialize(type: AnnotatedType, item: Duration, typeSupported: Predicate>): Any { + if (item.isInfinite()) { + throw SerializationException(Duration::class.java, "$item($type) is infinite and cannot be serialized") + } + + return "${formatNumber(item.toDouble(DurationUnit.SECONDS))}s" + } + + private fun formatNumber(value: Double): String { + return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt new file mode 100644 index 000000000..1b552b65c --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt @@ -0,0 +1,49 @@ +package dev.slne.surf.api.core.config.serializer + +import io.leangen.geantyref.GenericTypeReflector +import io.leangen.geantyref.TypeToken +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.util.EnumLookup +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Configurate scalar serializer for enum values. + * + * Enum constants are read using Configurate's enum lookup logic. If no direct match is found, + * underscores are converted to dashes and lookup is attempted again. + */ +internal object EnumValueSerializer : ScalarSerializer.Annotated>(object : TypeToken>() {}) { + private val LOGGER = ComponentLogger.logger() + + /** + * Resolves a serialized value to an enum constant of the requested enum type. + */ + override fun deserialize(type: AnnotatedType, obj: Any): Enum<*>? { + val constant = obj.toString() + val typeClass = GenericTypeReflector.erase(type.type).asSubclass(Enum::class.java) + + var foundEnum = EnumLookup.lookupEnum(typeClass, constant) + if (foundEnum == null) { + foundEnum = EnumLookup.lookupEnum(typeClass, constant.replace("_", "-")) + } + if (foundEnum == null) { + val joinedEnumOptions = typeClass.enumConstants.joinToString(limit = 10) + LOGGER.error( + "Invalid enum constant provided, expected one of [{}], but got {}", + joinedEnumOptions, + constant + ) + } + + return foundEnum + } + + /** + * Serializes an enum value using its constant name. + */ + override fun serialize(type: AnnotatedType, item: Enum<*>, typeSupported: Predicate>): Any { + return item.name + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/KeySerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/KeySerializer.kt new file mode 100644 index 000000000..d60cac238 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/KeySerializer.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.core.config.serializer + +import net.kyori.adventure.key.InvalidKeyException +import net.kyori.adventure.key.Key +import org.spongepowered.configurate.serialize.ScalarSerializer.Annotated +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Configurate scalar serializer for Adventure [Key] values. + * + * Values are read from and written to their canonical string representation. + */ +internal object KeySerializer : Annotated(Key::class.java) { + + /** + * Parses a string value into an Adventure [Key]. + */ + override fun deserialize(type: AnnotatedType, obj: Any): Key { + try { + return Key.key(obj.toString()) + } catch (e: InvalidKeyException) { + throw SerializationException(Key::class.java, e) + } + } + + /** + * Serializes [item] as its canonical string representation. + */ + override fun serialize(type: AnnotatedType, item: Key, typeSupported: Predicate>): Any { + return item.asString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt index 8dfb04f8a..5333619e7 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt @@ -1,23 +1,33 @@ package dev.slne.surf.api.core.config.serializer +import dev.slne.surf.api.core.config.constraints.MaxNumber +import dev.slne.surf.api.core.config.constraints.MinNumber +import dev.slne.surf.api.core.config.constraints.PositiveNumber +import dev.slne.surf.api.core.config.serializer.collection.map.FastutilMapSerializer +import dev.slne.surf.api.core.config.serializer.collection.map.MapSerializer +import dev.slne.surf.api.core.config.type.BooleanOrDefault +import dev.slne.surf.api.core.config.type.DurationOrDisabled +import dev.slne.surf.api.core.config.type.number.DoubleOr +import dev.slne.surf.api.core.config.type.number.IntOr import dev.slne.surf.api.core.minimessage.SurfMiniMessageHolder import dev.slne.surf.api.core.util.freeze import dev.slne.surf.api.core.util.mutableObject2ObjectMapOf import dev.slne.surf.api.core.util.requiredService import io.leangen.geantyref.TypeToken +import it.unimi.dsi.fastutil.ints.* +import it.unimi.dsi.fastutil.longs.* +import it.unimi.dsi.fastutil.objects.* import net.kyori.adventure.text.Component -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.kotlin.objectMapperFactory -import org.spongepowered.configurate.serialize.AbstractListChildSerializer -import org.spongepowered.configurate.serialize.SerializationException -import org.spongepowered.configurate.serialize.TypeSerializer -import org.spongepowered.configurate.serialize.TypeSerializerCollection +import org.spongepowered.configurate.kotlin.dataClassFieldDiscoverer +import org.spongepowered.configurate.kotlin.extensions.addConstraint +import org.spongepowered.configurate.objectmapping.ObjectMapper +import org.spongepowered.configurate.serialize.* import org.spongepowered.configurate.util.CheckedConsumer import java.lang.reflect.AnnotatedParameterizedType import java.lang.reflect.AnnotatedType -import java.lang.reflect.Type import java.util.* import java.util.function.Consumer +import java.util.function.Predicate val surfSpongeConfigSerializers = requiredService() @@ -32,7 +42,6 @@ abstract class SpongeConfigSerializers { val classSerializers get() = _classSerializers.freeze() init { - registerClassSerializer(Component::class.java, ComponentSerializer()) registerTypeTokenSerializer(LinkedListSerializer.TYPE, LinkedListSerializer()) } @@ -69,25 +78,92 @@ abstract class SpongeConfigSerializers { builder.register(type as TypeToken, serializer as TypeSerializer) } - builder.registerAnnotatedObjects(objectMapperFactory()) + builder.register(ComponentSerializer()) + builder.register(EnumValueSerializer) + builder.register(KeySerializer) + builder.register(DurationSerializer) + builder.register(BooleanOrDefault.Serializer) + builder.register(DurationOrDisabled.Serializer) + builder.register(IntOr.Default.Serializer) + builder.register(IntOr.Disabled.Serializer) + builder.register(DoubleOr.Default.Serializer) + builder.register(DoubleOr.Disabled.Serializer) + builder.register(MapSerializer.TYPE, MapSerializer(false)) + + //region fastutil maps + // @formatter:off + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2BooleanOpenHashMap(it as Map) }, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ByteOpenHashMap(it as Map) }, java.lang.Byte.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2CharOpenHashMap(it as Map) }, java.lang.Character.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2DoubleOpenHashMap(it as Map) }, java.lang.Double.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2FloatOpenHashMap(it as Map) }, java.lang.Float.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2IntOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2LongOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ShortOpenHashMap(it as Map) }, java.lang.Short.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Reference2ObjectOpenHashMap(it) })) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2BooleanOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2ByteOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2CharOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2DoubleOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2FloatOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.SomethingToSomething({ Int2IntOpenHashMap(it as Map) })) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2LongOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ObjectOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ReferenceOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2ShortOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2BooleanOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2ByteOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2CharOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2DoubleOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2FloatOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2IntOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.SomethingToSomething({ Long2LongOpenHashMap(it as Map) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ObjectOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ReferenceOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2ShortOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2BooleanOpenHashMap(it as Map) }, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ByteOpenHashMap(it as Map) }, java.lang.Byte.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2CharOpenHashMap(it as Map) }, java.lang.Character.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2DoubleOpenHashMap(it as Map) }, java.lang.Double.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2FloatOpenHashMap(it as Map) }, java.lang.Float.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2IntOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2LongOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ObjectOpenHashMap(it) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ReferenceOpenHashMap(it) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ShortOpenHashMap(it as Map) }, java.lang.Short.TYPE)) + // @formatter:on + //endregion + + builder.registerAnnotatedObjects( + ObjectMapper.factoryBuilder() + .addDiscoverer(dataClassFieldDiscoverer()) + .addConstraint(PositiveNumber.Companion.Factory) + .addConstraint(MinNumber.Companion.Factory) + .addConstraint(MaxNumber.Companion.Factory) + .build() + ) } /** - * Serializer for [Component] objects in Sponge configurations. + * Configurate scalar serializer for Adventure [Component] values. + * + * Components are serialized as MiniMessage strings and deserialized through the shared + * Surf MiniMessage instance. */ - class ComponentSerializer : TypeSerializer { - override fun deserialize(type: Type?, node: ConfigurationNode): Component { - val message = node.string ?: return Component.empty() + class ComponentSerializer : ScalarSerializer.Annotated(Component::class.java) { - return SurfMiniMessageHolder.miniMessage().deserialize(message) + /** + * Deserializes a MiniMessage string into a [Component]. + */ + override fun deserialize(type: AnnotatedType, obj: Any): Component { + return SurfMiniMessageHolder.miniMessage().deserialize(obj.toString()) } - override fun serialize(type: Type?, obj: Component?, node: ConfigurationNode) { - if (obj == null) { - return - } - - node.set(SurfMiniMessageHolder.miniMessage().serialize(obj)) + /** + * Serializes [item] into a MiniMessage string. + */ + override fun serialize(type: AnnotatedType, item: Component, typeSupported: Predicate>): Any { + return SurfMiniMessageHolder.miniMessage().serialize(item) } } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt new file mode 100644 index 000000000..4a7ec5527 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt @@ -0,0 +1,144 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +import io.leangen.geantyref.GenericTypeReflector +import io.leangen.geantyref.TypeFactory +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.TypeSerializer +import java.lang.reflect.AnnotatedParameterizedType +import java.lang.reflect.AnnotatedType +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +/** + * Serializer bridge for fastutil map implementations. + * + * Configurate does not need to know how to construct each fastutil map directly. Instead, + * this serializer maps the fastutil type to a regular [Map] type with boxed primitive + * key and value types, lets Configurate deserialize that regular map, and then creates + * the requested fastutil implementation via [factory]. + * + * Usage: + * ```kotlin + * builder.register( + * object : TypeToken>() {}, + * FastutilMapSerializer.SomethingToPrimitive>( + * { Object2IntOpenHashMap(it as Map) }, + * Integer.TYPE + * ) + * ) + * ``` + * + * @param M The concrete fastutil map type. + * @param factory Creates the concrete fastutil map from the deserialized regular map. + */ +abstract class FastutilMapSerializer>( + private val factory: (Map) -> M +) : TypeSerializer.Annotated { + + /** + * Deserializes the node as a regular boxed [Map] and converts it into the target + * fastutil map implementation. + */ + override fun deserialize(type: AnnotatedType, node: ConfigurationNode): M { + val mapType = createAnnotatedMapType(type as AnnotatedParameterizedType) + val map = node.get(mapType) as? Map ?: emptyMap() + + return factory(map) + } + + /** + * Serializes the fastutil map through a regular boxed [Map] type. + * + * Empty maps are written as `null`. + */ + override fun serialize(type: AnnotatedType, obj: M?, node: ConfigurationNode) { + if (obj.isNullOrEmpty()) { + node.raw(null) + return + } + + node.set(createAnnotatedMapType(type as AnnotatedParameterizedType), obj) + } + + + private fun createAnnotatedMapType(type: AnnotatedParameterizedType): AnnotatedType { + val baseType = createBaseMapType(type.type as ParameterizedType) + return GenericTypeReflector.annotate(baseType, type.annotations) + } + + /** + * Creates the regular boxed [Map] type that Configurate should use internally. + */ + protected abstract fun createBaseMapType(type: ParameterizedType): Type + + /** + * Serializer variant for maps with a regular object-like key and a primitive value. + * + * Example fastutil types: + * - `Object2IntMap` + * - `Reference2LongMap` + */ + class SomethingToPrimitive>( + factory: (Map) -> M, + private val primitiveType: Type + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: ParameterizedType): Type { + return TypeFactory.parameterizedClass( + Map::class.java, + type.actualTypeArguments[0], + GenericTypeReflector.box(primitiveType) + ) + } + } + + /** + * Serializer variant for maps with a primitive key and a regular object-like value. + * + * Example fastutil types: + * - `Int2ObjectMap` + * - `Long2ReferenceMap` + */ + class PrimitiveToSomething>( + factory: (Map) -> M, + private val primitiveType: Type + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: ParameterizedType): Type { + return TypeFactory.parameterizedClass( + Map::class.java, + GenericTypeReflector.box(primitiveType), + type.actualTypeArguments[0] + ) + } + } + + /** + * Serializer variant for maps where both key and value are represented by type arguments. + * + * Example fastutil types: + * - `Object2ObjectMap` + * - `Reference2ObjectMap` + */ + class SomethingToSomething>( + factory: (Map) -> M, + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: ParameterizedType): Type { + return TypeFactory.parameterizedClass( + Map::class.java, + type.actualTypeArguments[0], + type.actualTypeArguments[1] + ) + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt new file mode 100644 index 000000000..f7de468e9 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt @@ -0,0 +1,258 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +import io.leangen.geantyref.TypeToken +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import org.spongepowered.configurate.BasicConfigurationNode +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.ConfigurationOptions +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer +import org.spongepowered.configurate.serialize.TypeSerializerCollection +import java.lang.reflect.AnnotatedParameterizedType +import java.lang.reflect.AnnotatedType +import java.lang.reflect.Type + +/** + * Fault-tolerant Configurate serializer for maps. + * + * Invalid individual entries are logged and skipped instead of failing the entire map. + * If [ThrowExceptions] is present on the map type, this serializer delegates to Configurate's + * default map serializer and preserves the default exception behavior. + * + * When [clearInvalids] is enabled, keys that are present in the existing node but not written + * by the current serialized map are removed during serialization. + * + * @param clearInvalids Whether stale keys should be removed from the configuration node + * during serialization. + */ +internal class MapSerializer( + private val clearInvalids: Boolean +) : TypeSerializer.Annotated> { + companion object { + private val logger = ComponentLogger.logger() + + /** + * Type token used to register this serializer for all map types. + */ + val TYPE = object : TypeToken>() {} + } + private val fallback = requireNotNull(TypeSerializerCollection.defaults().get(TYPE)) { + "Could not find default Map serializer" + } + + /** + * Deserializes a map while skipping entries whose key or value cannot be deserialized. + * + * If [ThrowExceptions] is present on [type], the default Configurate map serializer is used. + */ + override fun deserialize(type: AnnotatedType, node: ConfigurationNode): Map<*, *> { + if (type.isAnnotationPresent(ThrowExceptions::class.java)) { + return fallback.deserialize(type, node) + } + + val map = linkedMapOf() + val rawType = type.type + + if (!node.isMap) { + return map + } + + if (type !is AnnotatedParameterizedType) { + throw SerializationException(rawType, "Raw types are not supported for collections") + } + + val args = type.annotatedActualTypeArguments + if (args.size != 2) { + throw SerializationException(rawType, "Map expected two type arguments!") + } + + val keyType = args[0] + val valueType = args[1] + + val keySerializer = node.options().serializers().get(keyType) + ?: throw SerializationException(rawType, "No type serializer available for key type $keyType") + + val valueSerializer = node.options().serializers().get(valueType) + ?: throw SerializationException(rawType, "No type serializer available for value type $valueType") + + val writeKeyBack = keyType.isAnnotationPresent(WriteKeyBack::class.java) + val keyNode = BasicConfigurationNode.root(node.options()) + val keysToClear = mutableSetOf() + + for ((rawKey, valueNode) in node.childrenMap()) { + val deserializedKey = deserializePart( + keyType.type, + keySerializer, + "key", + keyNode.set(rawKey), + node.path() + ) + + val deserializedValue = deserializePart( + valueType.type, + valueSerializer, + "value", + valueNode, + valueNode.path() + ) + + if (deserializedKey == null || deserializedValue == null) { + continue + } + + if (writeKeyBack) { + val shouldKeep = serializePart( + keyType.type, + keySerializer, + deserializedKey, + "key", + keyNode, + node.path() + ) + + val writtenKey = requireNotNull(keyNode.raw()) { "Key must not be null!" } + + if (shouldKeep && rawKey != writtenKey) { + keysToClear += rawKey + } + } + + map[deserializedKey] = deserializedValue + } + + + if (writeKeyBack) { + for (keyToClear in keysToClear) { + node.node(keyToClear).raw(null) + } + } + + return map + } + + /** + * Serializes a map into [node]. + * + * If [clearInvalids] is enabled, existing child nodes that were not visited while writing + * the current map are removed. + */ + override fun serialize(type: AnnotatedType, obj: Map<*, *>?, node: ConfigurationNode) { + if (type.isAnnotationPresent(ThrowExceptions::class.java)) { + fallback.serialize(type, obj, node) + return + } + + val rawType = type.type + + if (type !is AnnotatedParameterizedType) { + throw SerializationException(rawType, "Raw types are not supported for collections") + } + + val args = type.annotatedActualTypeArguments + if (args.size != 2) { + throw SerializationException(rawType, "Map expected two type arguments!") + } + + val keyType = args[0] + val valueType = args[1] + + val keySerializer = node.options().serializers().get(keyType) + ?: throw SerializationException(rawType, "No type serializer available for key type $keyType") + + val valueSerializer = node.options().serializers().get(valueType) + ?: throw SerializationException(rawType, "No type serializer available for value type $valueType") + + if (obj.isNullOrEmpty()) { + node.set(emptyMap()) + return + } + + val unvisitedKeys = if (node.empty()) { + node.raw(emptyMap()) + mutableSetOf() + } else { + node.childrenMap().keys.toMutableSet() + } + + val keyNode = BasicConfigurationNode.root(node.options()) + + for ((key, value) in obj) { + if (!serializePart(keyType.type, keySerializer, key, "key", keyNode, node.path())) { + continue + } + + val keyObj = requireNotNull(keyNode.raw()) { "Key must not be null!" } + val child = node.node(keyObj) + + serializePart(valueType.type, valueSerializer, value, "value", child, child.path()) + + unvisitedKeys -= keyObj + } + + if (clearInvalids) { + for (unusedChild in unvisitedKeys) { + node.removeChild(unusedChild) + } + } + } + + /** + * Returns an empty linked map for missing map values. + */ + override fun emptyValue(type: AnnotatedType, options: ConfigurationOptions): Map<*, *> { + if (type.isAnnotationPresent(ThrowExceptions::class.java)) { + return fallback.emptyValue(type, options) ?: linkedMapOf() + } + + return linkedMapOf() + } + + private fun deserializePart( + type: Type, + serializer: TypeSerializer<*>, + mapPart: String, + node: ConfigurationNode, + path: Any + ): Any? { + return try { + serializer.deserialize(type, node) + } catch (e: SerializationException) { + e.initPath(node::path) + logger.error( + "Could not deserialize {} {} into {} at {}: {}", + mapPart, + node.raw(), + type, + path, + e.rawMessage() + ) + null + } + } + + @Suppress("UNCHECKED_CAST") + private fun serializePart( + type: Type, + serializer: TypeSerializer<*>, + obj: Any?, + mapPart: String, + node: ConfigurationNode, + path: Any + ): Boolean { + return try { + (serializer as TypeSerializer).serialize(type, obj, node) + true + } catch (e: SerializationException) { + e.initPath(node::path) + logger.error( + "Could not serialize {} {} from {} at {}: {}", + mapPart, + obj, + type, + path, + e.rawMessage() + ) + false + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions.kt new file mode 100644 index 000000000..cbda94859 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +/** + * Forces [MapSerializer] to use Configurate's default map serializer behavior. + * + * Without this annotation, [MapSerializer] logs and skips individual invalid map entries. + * With this annotation, serialization and deserialization errors are thrown normally. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val strictMap: @ThrowExceptions Map = emptyMap() + * ) + * ``` + */ +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +annotation class ThrowExceptions \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack.kt new file mode 100644 index 000000000..3ab019c84 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.serializer.collection.map + +/** + * Writes normalized map keys back to the configuration after deserialization. + * + * This is useful for key types whose serializer accepts multiple input formats but serializes + * back into one canonical format. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val values: Map<@WriteKeyBack Key, Int> = emptyMap() + * ) + * ``` + * + * If the key serializer reads `minecraft:STONE` but serializes it as `minecraft:stone`, + * the old key is removed and the normalized key is written back. + */ +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +annotation class WriteKeyBack \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/BooleanOrDefault.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/BooleanOrDefault.kt new file mode 100644 index 000000000..75bfb8c22 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/BooleanOrDefault.kt @@ -0,0 +1,89 @@ +package dev.slne.surf.api.core.config.type + +import org.apache.commons.lang3.BooleanUtils +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Represents a boolean configuration value that can either be explicitly set to `true` or `false`, + * or defer to a caller-provided default value. + * + * The serialized configuration values are: + * - `true` + * - `false` + * - `default` + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class FeatureConfig( + * val enabled: BooleanOrDefault = BooleanOrDefault.USE_DEFAULT + * ) + * + * val enabled = config.enabled or globalDefaultEnabled + * ``` + */ +@ConsistentCopyVisibility +data class BooleanOrDefault private constructor(val value: Boolean?) { + + /** + * Returns the configured boolean value, or [other] if this value is configured as `default`. + */ + infix fun or(other: Boolean) = value ?: other + + companion object { + + /** + * Represents the `default` configuration value. + */ + @JvmField + val USE_DEFAULT = BooleanOrDefault(null) + + /** + * Represents an explicitly enabled configuration value. + */ + @JvmField + val TRUE = BooleanOrDefault(true) + + /** + * Represents an explicitly disabled configuration value. + */ + @JvmField + val FALSE = BooleanOrDefault(false) + } + + /** + * Configurate serializer for [BooleanOrDefault]. + */ + internal object Serializer : ScalarSerializer.Annotated(BooleanOrDefault::class.java) { + private const val DEFAULT_VALUE = "default" + + override fun deserialize(type: AnnotatedType, obj: Any): BooleanOrDefault { + if (obj is String) { + if (obj.equals(DEFAULT_VALUE, ignoreCase = true)) { + return USE_DEFAULT + } + + try { + return BooleanOrDefault(BooleanUtils.toBoolean(obj.lowercase(), "true", "false")) + } catch (e: IllegalArgumentException) { + throw SerializationException( + BooleanOrDefault::class.java, + "$obj($type) is not a boolean or '$DEFAULT_VALUE'", + e + ) + } + } else if (obj is Boolean) { + return BooleanOrDefault(obj) + } + + throw SerializationException(BooleanOrDefault::class.java, "$obj($type) is not a boolean or '$DEFAULT_VALUE'") + } + + override fun serialize(type: AnnotatedType, item: BooleanOrDefault, typeSupported: Predicate>): Any { + return item.value ?: DEFAULT_VALUE + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt new file mode 100644 index 000000000..484ad80f9 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt @@ -0,0 +1,95 @@ +package dev.slne.surf.api.core.config.type + +import dev.slne.surf.api.core.config.serializer.DurationSerializer +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import kotlin.time.Duration + +/** + * Represents a duration configuration value that can either contain a Kotlin [Duration], + * or be explicitly disabled. + * + * The serialized configuration values are: + * - duration strings such as `10s`, `5m`, `2h`, or `1d` + * - `disabled` + * + * Usage: + * ```kotlin + * import kotlin.time.Duration.Companion.seconds + * + * @ConfigSerializable + * data class TimeoutConfig( + * val timeout: DurationOrDisabled = DurationOrDisabled.DISABLED + * ) + * + * val timeout = config.timeout or 30.seconds + * + * if (config.timeout.isDisabled()) { + * // Disable timeout handling + * } + * ``` + */ +@ConsistentCopyVisibility +data class DurationOrDisabled private constructor(val value: Duration?) { + + /** + * Returns the configured duration, or [other] if this value is disabled. + */ + infix fun or(other: Duration) = value ?: other + + /** + * Returns `true` if this value is configured as `disabled`. + */ + fun isDisabled() = value == null + + companion object { + private const val DISABLED_VALUE = "disabled" + + /** + * Represents the `disabled` configuration value. + */ + @JvmField + val DISABLED = DurationOrDisabled(null) + } + + /** + * Configurate serializer for [DurationOrDisabled]. + */ + internal object Serializer : ScalarSerializer.Annotated(DurationOrDisabled::class.java) { + + override fun deserialize(type: AnnotatedType, obj: Any): DurationOrDisabled { + if (obj is String) { + if (obj.equals(DISABLED_VALUE, ignoreCase = true)) { + return DISABLED + } + + return try { + DurationOrDisabled( + DurationSerializer.deserialize(type, obj) + ) + } catch (e: Exception) { + throw SerializationException( + DurationOrDisabled::class.java, + "$obj($type) is not a duration or '$DISABLED_VALUE'", + e + ) + } + } + + throw SerializationException( + DurationOrDisabled::class.java, + "$obj($type) is not a duration or '$DISABLED_VALUE'" + ) + } + + override fun serialize( + type: AnnotatedType, + item: DurationOrDisabled, + typeSupported: Predicate> + ): Any { + return item.value?.let { DurationSerializer.serialize(type, it, typeSupported) } ?: DISABLED_VALUE + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty.kt new file mode 100644 index 000000000..282bffb45 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.type.number + +/** + * Marks an optional numeric config value so that negative values are treated as the empty state. + * + * This is mainly useful for `IntOr` and `DoubleOr` values where old or user-provided + * negative values should behave like `default` or `disabled`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class ViewConfig( + * @field:BelowZeroToEmpty + * val viewDistance: IntOr.Default = IntOr.Default.USE_DEFAULT + * ) + * ``` + * + * In this example, `view-distance: -1` is interpreted as `default`. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class BelowZeroToEmpty \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/DoubleOr.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/DoubleOr.kt new file mode 100644 index 000000000..e8896063d --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/DoubleOr.kt @@ -0,0 +1,197 @@ +package dev.slne.surf.api.core.config.type.number + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Represents an optional double configuration value. + * + * Implementations decide what the empty state means: + * - [Default] uses `default` + * - [Disabled] uses `disabled` + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class SpawnConfig( + * val multiplier: DoubleOr.Default = DoubleOr.Default.USE_DEFAULT, + * val customChance: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED + * ) + * + * val multiplier = config.multiplier or 1.0 + * + * if (config.customChance.test { it > 0.0 }) { + * val chance = config.customChance.doubleValue() + * } + * ``` + */ +interface DoubleOr { + + /** + * Returns the configured double value, or [fallback] if this value is empty. + */ + infix fun or(fallback: Double): Double + + /** + * The configured double value, or `null` if this value is empty. + */ + val value: Double? + + /** + * Returns the configured double value. + * + * @throws NullPointerException if this value is empty. + */ + fun doubleValue(): Double = value!! + + /** + * Double value that can be configured as either a concrete number or `default`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val multiplier: DoubleOr.Default = DoubleOr.Default.USE_DEFAULT + * ) + * + * val multiplier = config.multiplier or 1.0 + * ``` + */ + data class Default( + override val value: Double? + ) : DoubleOr { + + /** + * Returns the configured double value, or [fallback] if this value is `default`. + */ + override fun or(fallback: Double) = value ?: fallback + + companion object { + private const val DEFAULT_VALUE = "default" + + /** + * Represents the `default` configuration value. + */ + @JvmField + val USE_DEFAULT = Default(null) + } + + /** + * Configurate serializer for [Default]. + */ + internal object Serializer : ScalarSerializer.Annotated(Default::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Default { + val value = parseDouble(type, obj, DEFAULT_VALUE) ?: return USE_DEFAULT + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return USE_DEFAULT + } + + return Default(value) + } + + override fun serialize( + type: AnnotatedType, + item: Default, + typeSupported: Predicate> + ): Any { + return item.value ?: DEFAULT_VALUE + } + } + } + + /** + * Double value that can be configured as either a concrete number or `disabled`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val threshold: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED + * ) + * + * if (config.threshold.enabled()) { + * val threshold = config.threshold.doubleValue() + * } + * ``` + */ + data class Disabled( + override val value: Double? + ) : DoubleOr { + + /** + * Returns the configured double value, or [fallback] if this value is `disabled`. + */ + override fun or(fallback: Double) = value ?: fallback + + /** + * Returns `true` if this value contains an explicit double. + */ + fun enabled(): Boolean = value != null + + /** + * Returns `true` if this value is enabled and its double value matches [predicate]. + */ + fun test(predicate: (Double) -> Boolean): Boolean { + return value?.let(predicate) ?: false + } + + companion object { + private const val DISABLED_VALUE = "disabled" + + /** + * Represents the `disabled` configuration value. + */ + @JvmField + val DISABLED = Disabled(null) + } + + /** + * Configurate serializer for [Disabled]. + */ + object Serializer : ScalarSerializer.Annotated(Disabled::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Disabled { + val value = parseDouble(type, obj, DISABLED_VALUE) ?: return DISABLED + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return DISABLED + } + + return Disabled(value) + } + + override fun serialize( + type: AnnotatedType, + item: Disabled, + typeSupported: Predicate> + ): Any { + return item.value ?: DISABLED_VALUE + } + } + } +} + +private fun parseDouble(type: AnnotatedType, obj: Any, emptyValue: String): Double? { + return when (obj) { + is String -> { + if (obj.equals(emptyValue, ignoreCase = true)) { + null + } else { + obj.toDoubleOrNull() + ?: throw SerializationException( + Double::class.java, + "$obj($type) is not a double or '$emptyValue'" + ) + } + } + + is Number -> obj.toDouble() + + else -> throw SerializationException( + Double::class.java, + "$obj($type) is not a double or '$emptyValue'" + ) + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/IntOr.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/IntOr.kt new file mode 100644 index 000000000..2be56d520 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/number/IntOr.kt @@ -0,0 +1,199 @@ +package dev.slne.surf.api.core.config.type.number + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +/** + * Represents an optional integer configuration value. + * + * Implementations decide what the empty state means: + * - [Default] uses `default` + * - [Disabled] uses `disabled` + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class RetryConfig( + * val maxRetries: IntOr.Default = IntOr.Default.USE_DEFAULT, + * val queueLimit: IntOr.Disabled = IntOr.Disabled.DISABLED + * ) + * + * val retries = config.maxRetries or 3 + * + * if (config.queueLimit.enabled()) { + * val limit = config.queueLimit.intValue() + * } + * ``` + */ +interface IntOr { + + /** + * Returns the configured integer value, or [fallback] if this value is empty. + */ + infix fun or(fallback: Int): Int + + /** + * The configured integer value, or `null` if this value is empty. + */ + val value: Int? + + /** + * Returns `true` if this value contains an explicit integer. + */ + fun isDefined(): Boolean = value != null + + /** + * Returns the configured integer value. + * + * @throws NullPointerException if this value is empty. + */ + fun intValue(): Int = value!! + + /** + * Integer value that can be configured as either a concrete number or `default`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val amount: IntOr.Default = IntOr.Default.USE_DEFAULT + * ) + * + * val amount = config.amount or 10 + * ``` + */ + data class Default(override val value: Int?) : IntOr { + + /** + * Returns the configured integer value, or [fallback] if this value is `default`. + */ + override fun or(fallback: Int) = value ?: fallback + + companion object { + private const val DEFAULT_VALUE = "default" + + /** + * Represents the `default` configuration value. + */ + @JvmField + val USE_DEFAULT = Default(null) + } + + /** + * Configurate serializer for [Default]. + */ + internal object Serializer : ScalarSerializer.Annotated(Default::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Default { + val value = parseInt(type, obj, DEFAULT_VALUE) ?: return USE_DEFAULT + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return USE_DEFAULT + } + + return Default(value) + } + + override fun serialize( + type: AnnotatedType, + item: Default, + typeSupported: Predicate> + ): Any { + return item.value ?: DEFAULT_VALUE + } + } + } + + /** + * Integer value that can be configured as either a concrete number or `disabled`. + * + * Usage: + * ```kotlin + * @ConfigSerializable + * data class Config( + * val limit: IntOr.Disabled = IntOr.Disabled.DISABLED + * ) + * + * if (config.limit.enabled()) { + * val limit = config.limit.intValue() + * } + * ``` + */ + data class Disabled( + override val value: Int? + ) : IntOr { + /** + * Returns the configured integer value, or [fallback] if this value is `disabled`. + */ + override fun or(fallback: Int) = value ?: fallback + + /** + * Returns `true` if this value contains an explicit integer. + */ + fun enabled(): Boolean = value != null + + /** + * Returns `true` if this value is enabled and its integer value matches [predicate]. + */ + inline fun test(predicate: (Int) -> Boolean): Boolean { + return value?.let(predicate) ?: false + } + + companion object { + private const val DISABLED_VALUE = "disabled" + + /** + * Represents the `disabled` configuration value. + */ + @JvmField + val DISABLED = Disabled(null) + } + + /** + * Configurate serializer for [Disabled]. + */ + internal object Serializer : ScalarSerializer.Annotated(Disabled::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Disabled { + val value = parseInt(type, obj, DISABLED_VALUE) ?: return DISABLED + + if (type.isAnnotationPresent(BelowZeroToEmpty::class.java) && value < 0) { + return DISABLED + } + + return Disabled(value) + } + + override fun serialize( + type: AnnotatedType, + item: Disabled, + typeSupported: Predicate> + ): Any { + return item.value ?: DISABLED_VALUE + } + } + } +} + +private fun parseInt(type: AnnotatedType, obj: Any, emptyValue: String): Int? { + return when (obj) { + is String -> { + if (obj.equals(emptyValue, ignoreCase = true)) { + null + } else { + obj.toIntOrNull() + ?: throw SerializationException( + Int::class.java, + "$obj($type) is not an int or '$emptyValue'" + ) + } + } + + is Number -> obj.toInt() + + else -> throw SerializationException( + Int::class.java, + "$obj($type) is not an int or '$emptyValue'" + ) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java index c645947a0..be64f2c52 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/config/TestConfig2.java @@ -1,5 +1,6 @@ package dev.slne.surf.api.paper.test.config; +import dev.slne.surf.api.core.config.type.BooleanOrDefault; import org.spongepowered.configurate.objectmapping.ConfigSerializable; import org.spongepowered.configurate.objectmapping.meta.Comment; import org.spongepowered.configurate.objectmapping.meta.Matches; @@ -13,6 +14,8 @@ public class TestConfig2 { @Setting("connection") public ConnectionConfig connectionConfig = new ConnectionConfig(); + public BooleanOrDefault testBooleanOrDefault = BooleanOrDefault.USE_DEFAULT; + @ConfigSerializable public static class ConnectionConfig { diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt index 1091e13d1..da5333818 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernTestConfig.kt @@ -1,6 +1,7 @@ package dev.slne.surf.surfapi.bukkit.test.config import dev.slne.surf.api.core.config.SpongeYmlConfigClass +import dev.slne.surf.api.core.config.type.BooleanOrDefault import dev.slne.surf.surfapi.bukkit.test.plugin import org.spongepowered.configurate.objectmapping.ConfigSerializable import org.spongepowered.configurate.objectmapping.meta.Comment @@ -10,7 +11,8 @@ data class ModernTestConfig( @Comment("This is a modern config!") var message: String = "Hello from Modern Config!", var number: Int = 42, - var enabled: Boolean = true + var enabled: Boolean = true, + var booleanOrDefault: BooleanOrDefault = BooleanOrDefault.TRUE, ) { companion object : SpongeYmlConfigClass( ModernTestConfig::class.java, @@ -20,7 +22,13 @@ data class ModernTestConfig( fun randomise() = edit { message = "Random Message ${Math.random()}" number = (Math.random() * 100).toInt() + // enabled = Math.random() > 0.5 } } -} \ No newline at end of file +} + +@ConfigSerializable +data class ModernTestConfig2( + val map: Map = emptyMap() +) \ No newline at end of file From 63bee172f8986d3123306fad3125b79819cbec55 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 13:13:27 +0200 Subject: [PATCH 02/18] =?UTF-8?q?```=20=F0=9F=94=A7=20chore:=20update=20ve?= =?UTF-8?q?rsion=20to=203.12.0=20in=20gradle.properties=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index de8d5e8f9..f1e646c9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.1.2 group=dev.slne.surf.api -version=3.11.2 +version=3.12.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false From 3c9eed678a7e4aa024a8f6289ec53bebe6ba7cc0 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 13:14:59 +0200 Subject: [PATCH 03/18] =?UTF-8?q?=F0=9F=94=A7=20chore(abi):=20update=20api?= =?UTF-8?q?=20dump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../surf-api-core/api/surf-api-core.api | 202 +++++++++++++++++- 1 file changed, 197 insertions(+), 5 deletions(-) diff --git a/surf-api-core/surf-api-core/api/surf-api-core.api b/surf-api-core/surf-api-core/api/surf-api-core.api index e2c721426..995c79c50 100644 --- a/surf-api-core/surf-api-core/api/surf-api-core.api +++ b/surf-api-core/surf-api-core/api/surf-api-core.api @@ -207,6 +207,29 @@ public final class dev/slne/surf/api/core/config/SurfConfigApiKt { public abstract interface annotation class dev/slne/surf/api/core/config/YamlConfigFileNamePattern : java/lang/annotation/Annotation { } +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxNumber$Companion; + public abstract fun max ()D +} + +public final class dev/slne/surf/api/core/config/constraints/MaxNumber$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinNumber$Companion; + public abstract fun min ()D +} + +public final class dev/slne/surf/api/core/config/constraints/MinNumber$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/PositiveNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/PositiveNumber$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/PositiveNumber$Companion { +} + public final class dev/slne/surf/api/core/config/manager/LoadConfigException : java/lang/RuntimeException { public static final field Companion Ldev/slne/surf/api/core/config/manager/LoadConfigException$Companion; public fun (Ljava/lang/String;)V @@ -292,12 +315,11 @@ public abstract class dev/slne/surf/api/core/config/serializer/SpongeConfigSeria public final fun unregisterTypeTokenSerializer (Lio/leangen/geantyref/TypeToken;)V } -public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers$ComponentSerializer : org/spongepowered/configurate/serialize/TypeSerializer { +public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers$ComponentSerializer : org/spongepowered/configurate/serialize/ScalarSerializer$Annotated { public fun ()V - public synthetic fun deserialize (Ljava/lang/reflect/Type;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/lang/Object; - public fun deserialize (Ljava/lang/reflect/Type;Lorg/spongepowered/configurate/ConfigurationNode;)Lnet/kyori/adventure/text/Component; - public synthetic fun serialize (Ljava/lang/reflect/Type;Ljava/lang/Object;Lorg/spongepowered/configurate/ConfigurationNode;)V - public fun serialize (Ljava/lang/reflect/Type;Lnet/kyori/adventure/text/Component;Lorg/spongepowered/configurate/ConfigurationNode;)V + public synthetic fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Ljava/lang/Object; + public fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Lnet/kyori/adventure/text/Component; + public synthetic fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;Ljava/util/function/Predicate;)Ljava/lang/Object; } public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers$LinkedListSerializer : org/spongepowered/configurate/serialize/AbstractListChildSerializer { @@ -316,6 +338,176 @@ public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializ public static final fun getSurfSpongeConfigSerializers ()Ldev/slne/surf/api/core/config/serializer/SpongeConfigSerializers; } +public abstract class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer : org/spongepowered/configurate/serialize/TypeSerializer$Annotated { + public fun (Lkotlin/jvm/functions/Function1;)V + protected abstract fun createBaseMapType (Ljava/lang/reflect/ParameterizedType;)Ljava/lang/reflect/Type; + public synthetic fun deserialize (Ljava/lang/reflect/AnnotatedType;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/lang/Object; + public fun deserialize (Ljava/lang/reflect/AnnotatedType;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/util/Map; + public synthetic fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;Lorg/spongepowered/configurate/ConfigurationNode;)V + public fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/util/Map;Lorg/spongepowered/configurate/ConfigurationNode;)V +} + +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$PrimitiveToSomething : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/reflect/Type;)V +} + +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$SomethingToPrimitive : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/reflect/Type;)V +} + +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$SomethingToSomething : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;)V +} + +public abstract interface annotation class dev/slne/surf/api/core/config/serializer/collection/map/ThrowExceptions : java/lang/annotation/Annotation { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/serializer/collection/map/WriteKeyBack : java/lang/annotation/Annotation { +} + +public final class dev/slne/surf/api/core/config/type/BooleanOrDefault { + public static final field Companion Ldev/slne/surf/api/core/config/type/BooleanOrDefault$Companion; + public static final field FALSE Ldev/slne/surf/api/core/config/type/BooleanOrDefault; + public static final field TRUE Ldev/slne/surf/api/core/config/type/BooleanOrDefault; + public static final field USE_DEFAULT Ldev/slne/surf/api/core/config/type/BooleanOrDefault; + public synthetic fun (Ljava/lang/Boolean;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/Boolean; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()Ljava/lang/Boolean; + public fun hashCode ()I + public final fun or (Z)Z + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/BooleanOrDefault$Companion { +} + +public final class dev/slne/surf/api/core/config/type/DurationOrDisabled { + public static final field Companion Ldev/slne/surf/api/core/config/type/DurationOrDisabled$Companion; + public static final field DISABLED Ldev/slne/surf/api/core/config/type/DurationOrDisabled; + public synthetic fun (Lkotlin/time/Duration;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-FghU774 ()Lkotlin/time/Duration; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue-FghU774 ()Lkotlin/time/Duration; + public fun hashCode ()I + public final fun isDisabled ()Z + public final fun or-wmV0flA (J)J + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/DurationOrDisabled$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/type/number/BelowZeroToEmpty : java/lang/annotation/Annotation { +} + +public abstract interface class dev/slne/surf/api/core/config/type/number/DoubleOr { + public fun doubleValue ()D + public abstract fun getValue ()Ljava/lang/Double; + public abstract fun or (D)D +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Default : dev/slne/surf/api/core/config/type/number/DoubleOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default$Companion; + public static final field USE_DEFAULT Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default; + public fun (Ljava/lang/Double;)V + public final fun component1 ()Ljava/lang/Double; + public final fun copy (Ljava/lang/Double;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default;Ljava/lang/Double;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Default; + public fun doubleValue ()D + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Double; + public fun hashCode ()I + public fun or (D)D + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Default$Companion { +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$DefaultImpls { + public static fun doubleValue (Ldev/slne/surf/api/core/config/type/number/DoubleOr;)D +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Disabled : dev/slne/surf/api/core/config/type/number/DoubleOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Companion; + public static final field DISABLED Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public fun (Ljava/lang/Double;)V + public final fun component1 ()Ljava/lang/Double; + public final fun copy (Ljava/lang/Double;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled;Ljava/lang/Double;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public fun doubleValue ()D + public final fun enabled ()Z + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Double; + public fun hashCode ()I + public fun or (D)D + public final fun test (Lkotlin/jvm/functions/Function1;)Z + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Companion { +} + +public final class dev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Serializer : org/spongepowered/configurate/serialize/ScalarSerializer$Annotated { + public static final field INSTANCE Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled$Serializer; + public fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/DoubleOr$Disabled; + public synthetic fun deserialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;Ljava/util/function/Predicate;)Ljava/lang/Object; +} + +public abstract interface class dev/slne/surf/api/core/config/type/number/IntOr { + public abstract fun getValue ()Ljava/lang/Integer; + public fun intValue ()I + public fun isDefined ()Z + public abstract fun or (I)I +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Default : dev/slne/surf/api/core/config/type/number/IntOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/IntOr$Default$Companion; + public static final field USE_DEFAULT Ldev/slne/surf/api/core/config/type/number/IntOr$Default; + public fun (Ljava/lang/Integer;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/Integer;)Ldev/slne/surf/api/core/config/type/number/IntOr$Default; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/IntOr$Default;Ljava/lang/Integer;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/IntOr$Default; + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Integer; + public fun hashCode ()I + public fun intValue ()I + public fun isDefined ()Z + public fun or (I)I + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Default$Companion { +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$DefaultImpls { + public static fun intValue (Ldev/slne/surf/api/core/config/type/number/IntOr;)I + public static fun isDefined (Ldev/slne/surf/api/core/config/type/number/IntOr;)Z +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Disabled : dev/slne/surf/api/core/config/type/number/IntOr { + public static final field Companion Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled$Companion; + public static final field DISABLED Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled; + public fun (Ljava/lang/Integer;)V + public final fun component1 ()Ljava/lang/Integer; + public final fun copy (Ljava/lang/Integer;)Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled; + public static synthetic fun copy$default (Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled;Ljava/lang/Integer;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/number/IntOr$Disabled; + public final fun enabled ()Z + public fun equals (Ljava/lang/Object;)Z + public fun getValue ()Ljava/lang/Integer; + public fun hashCode ()I + public fun intValue ()I + public fun isDefined ()Z + public fun or (I)I + public final fun test (Lkotlin/jvm/functions/Function1;)Z + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/number/IntOr$Disabled$Companion { +} + public abstract class dev/slne/surf/api/core/event/SurfAsyncEvent : dev/slne/surf/api/core/event/SurfEvent { public fun ()V public final fun call (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; From d239a3f2a012b148ff485ea317463c3f59a47ab7 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 20:37:05 +0200 Subject: [PATCH 04/18] =?UTF-8?q?=E2=9C=A8=20feat(config):=20add=20seriali?= =?UTF-8?q?zers=20for=20various=20configuration=20types=20and=20constraint?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - implement serializers for BukkitConfigurationSerializable, ItemStack, Location, Material, and others - introduce new constraints like Contains, Directory, DisallowValues, and more - enhance configuration handling with custom serializers for better data management --- .../slne/surf/api/core/server/CoreInstance.kt | 5 + .../constraints/CollectionConstraintUtils.kt | 10 ++ .../api/core/config/constraints/Contains.kt | 19 +++ .../api/core/config/constraints/Directory.kt | 22 ++++ .../core/config/constraints/DisallowValues.kt | 19 +++ .../api/core/config/constraints/EndsWith.kt | 19 +++ .../core/config/constraints/ExistingFile.kt | 34 ++++++ .../core/config/constraints/MaxDuration.kt | 20 ++++ .../api/core/config/constraints/MaxLength.kt | 19 +++ .../api/core/config/constraints/MaxSize.kt | 20 ++++ .../core/config/constraints/MinDuration.kt | 20 ++++ .../api/core/config/constraints/MinLength.kt | 27 +++++ .../api/core/config/constraints/MinSize.kt | 20 ++++ .../api/core/config/constraints/Namespace.kt | 20 ++++ .../core/config/constraints/NegativeNumber.kt | 19 +++ .../core/config/constraints/NoDuplicates.kt | 26 ++++ .../api/core/config/constraints/NotBlank.kt | 28 +++++ .../api/core/config/constraints/NotEmpty.kt | 20 ++++ .../surf/api/core/config/constraints/Range.kt | 31 +++++ .../api/core/config/constraints/StartsWith.kt | 19 +++ .../api/core/config/constraints/Trimmed.kt | 30 +++++ .../core/config/constraints/WritablePath.kt | 22 ++++ .../core/config/serializer/FileSerializer.kt | 16 +++ .../core/config/serializer/PathSerializer.kt | 16 +++ .../config/serializer/PatternSerializer.kt | 22 ++++ .../core/config/serializer/RegexSerializer.kt | 21 ++++ .../serializer/SpongeConfigSerializers.kt | 39 +++++- .../config/serializer/TextColorSerializer.kt | 34 ++++++ .../core/config/serializer/UriSerializer.kt | 22 ++++ .../core/config/serializer/UrlSerializer.kt | 23 ++++ .../core/config/serializer/UuidSerializer.kt | 21 ++++ ...kkitConfigurationSerializableSerializer.kt | 42 +++++++ .../config/serializers/ItemStackSerializer.kt | 23 ++++ .../config/serializers/LocationSerializer.kt | 42 +++++++ .../config/serializers/MaterialSerializer.kt | 20 ++++ .../serializers/NamespacedKeySerializer.kt | 18 +++ .../PaperSpongeConfigSerializers.kt | 112 +++++++++++++----- .../config/serializers/VectorSerializer.kt | 27 +++++ .../registry/RegistryEntrySerializer.kt | 66 +++++++++++ .../registry/RegistryValueSerializer.kt | 32 +++++ 40 files changed, 1031 insertions(+), 34 deletions(-) create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/CollectionConstraintUtils.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinLength.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotBlank.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Range.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Trimmed.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/FileSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PatternSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/RegexSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/TextColorSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UriSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UrlSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UuidSerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/BukkitConfigurationSerializableSerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/MaterialSerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/NamespacedKeySerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt create mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt diff --git a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt index 71faad8aa..9d02aa633 100644 --- a/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt +++ b/surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt @@ -4,8 +4,10 @@ import dev.slne.surf.api.core.messages.Colors import dev.slne.surf.api.core.server.listener.CoreListenerManager import dev.slne.surf.api.core.server.util.PlayerSkinFetcher import org.jetbrains.annotations.MustBeInvokedByOverriders +import java.util.concurrent.atomic.AtomicBoolean abstract class CoreInstance { + private val bootstrapping = AtomicBoolean(true) @MustBeInvokedByOverriders open suspend fun bootstrap() { @@ -14,6 +16,7 @@ abstract class CoreInstance { @MustBeInvokedByOverriders open suspend fun onLoad() { + bootstrapping.set(false) } @MustBeInvokedByOverriders @@ -30,4 +33,6 @@ abstract class CoreInstance { PlayerSkinFetcher Colors } + + fun isBootstrapping(): Boolean = bootstrapping.get() } \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/CollectionConstraintUtils.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/CollectionConstraintUtils.kt new file mode 100644 index 000000000..b1f571f8f --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/CollectionConstraintUtils.kt @@ -0,0 +1,10 @@ +package dev.slne.surf.api.core.config.constraints + +internal fun Any?.configSizeOrNull(): Int? = when (this) { + null -> null + is Collection<*> -> size + is Map<*, *> -> size + is Array<*> -> size + is CharSequence -> length + else -> null +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt new file mode 100644 index 000000000..5b96f7f4e --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Contains(val value: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Contains, type: Type): Constraint = { value -> + if (value != null && !value.contains(data.value)) { + throw SerializationException("String must contain '${data.value}'") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt new file mode 100644 index 000000000..788ded6ec --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type +import kotlin.io.path.exists +import kotlin.io.path.isDirectory + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Directory { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Directory, type: Type): Constraint = Constraint { value -> + val path = value.asPathOrNull() ?: return@Constraint + if (!path.exists() || !path.isDirectory()) { + throw SerializationException("Path must point to an existing directory: $path") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt new file mode 100644 index 000000000..9dbbbb433 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class DisallowValues(vararg val values: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: DisallowValues, type: Type): Constraint = { value -> + if (value != null && data.values.any { it.equals(value.toString(), ignoreCase = true) }) { + throw SerializationException("Value '$value' is not allowed") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt new file mode 100644 index 000000000..56ba8c3d1 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class EndsWith(val suffix: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: EndsWith, type: Type): Constraint = { value -> + if (value != null && !value.endsWith(data.suffix)) { + throw SerializationException("String must end with '${data.suffix}'") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt new file mode 100644 index 000000000..7a7c49a19 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.io.File +import java.lang.reflect.Type +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class ExistingFile { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: ExistingFile, type: Type): Constraint = Constraint { value -> + val path = value.asPathOrNull() ?: return@Constraint + if (!path.exists() || !path.isRegularFile()) { + throw SerializationException("Path must point to an existing file: $path") + } + } + } + } +} + +internal fun Any?.asPathOrNull(): Path? { + return when (this) { + null -> null + is Path -> this + is File -> toPath() + is String -> Path.of(this) + else -> null + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt new file mode 100644 index 000000000..80dec99c5 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type +import kotlin.time.Duration + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxDuration(val seconds: Long) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MaxDuration, type: Type): Constraint = { value -> + if (value != null && value.inWholeSeconds > data.seconds) { + throw SerializationException("Duration is too long: $value, expected <= ${data.seconds}s") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt new file mode 100644 index 000000000..92cb302e3 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxLength(val max: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MaxLength, type: Type): Constraint = { value -> + if (value != null && value.length > data.max) { + throw SerializationException("String is too long: ${value.length}, expected <= ${data.max}") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt new file mode 100644 index 000000000..b57e326c3 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MaxSize(val max: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MaxSize, type: Type): Constraint = { value -> + val size = value.configSizeOrNull() + if (size != null && size > data.max) { + throw SerializationException("Collection size is too large: $size, expected <= ${data.max}") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt new file mode 100644 index 000000000..12145717e --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type +import kotlin.time.Duration + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinDuration(val seconds: Long) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MinDuration, type: Type): Constraint = { value -> + if (value != null && value.inWholeSeconds < data.seconds) { + throw SerializationException("Duration is too short: $value, expected >= ${data.seconds}s") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinLength.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinLength.kt new file mode 100644 index 000000000..81f5985bd --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinLength.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Ensures that a string configuration value meets a minimum length requirement. + * + * The string is considered valid if its length is greater than or equal to the specified [min] value. + * Null values are ignored, making this annotation compatible with nullable string fields. + * + * @property min The inclusive minimum number of characters required for the string. + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinLength(val min: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MinLength, type: Type): Constraint = { value -> + if (value != null && value.length < data.min) { + throw SerializationException("String is too short: ${value.length}, expected >= ${data.min}") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt new file mode 100644 index 000000000..106e3f811 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class MinSize(val min: Int) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: MinSize, type: Type): Constraint = { value -> + val size = value.configSizeOrNull() + if (size != null && size < data.min) { + throw SerializationException("Collection size is too small: $size, expected >= ${data.min}") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt new file mode 100644 index 000000000..c33865ddd --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.core.config.constraints + +import net.kyori.adventure.key.Key +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Namespace(val namespace: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Namespace, type: Type): Constraint = { value -> + if (value != null && value.namespace() != data.namespace) { + throw SerializationException("Key must use namespace '${data.namespace}', got '${value.namespace()}'") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt new file mode 100644 index 000000000..5d783d911 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NegativeNumber { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NegativeNumber, type: Type): Constraint = { value -> + if (value != null && value.toDouble() >= 0.0) { + throw SerializationException("Number must be negative: $value, expected < 0") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt new file mode 100644 index 000000000..28ae969b0 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt @@ -0,0 +1,26 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NoDuplicates { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NoDuplicates, type: Type): Constraint = Constraint { value -> + val elements = when (value) { + null -> return@Constraint + is Iterable<*> -> value.toList() + is Array<*> -> value.toList() + else -> return@Constraint + } + + if (elements.size != elements.toSet().size) { + throw SerializationException("Collection must not contain duplicate values") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotBlank.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotBlank.kt new file mode 100644 index 000000000..b862e595c --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotBlank.kt @@ -0,0 +1,28 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Requires a string config value to contain at least one non-whitespace character. + * + * Usage: + * ```kotlin + * @field:NotBlank + * val name: String = "default" + * ``` + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NotBlank { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NotBlank, type: Type): Constraint = { value -> + if (value != null && value.isBlank()) { + throw SerializationException("String must not be blank") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt new file mode 100644 index 000000000..9c4847136 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class NotEmpty { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: NotEmpty, type: Type): Constraint = { value -> + val size = value.configSizeOrNull() + if (size != null && size == 0) { + throw SerializationException("Value must not be empty") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Range.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Range.kt new file mode 100644 index 000000000..26ca037c5 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Range.kt @@ -0,0 +1,31 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Requires a numeric config value to be inside an inclusive range. + * + * Usage: + * ```kotlin + * @field:Range(min = 0.0, max = 1.0) + * val chance: Double = 0.5 + * ``` + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Range(val min: Double, val max: Double) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Range, type: Type): Constraint = { value -> + if (value != null) { + val double = value.toDouble() + if (double < data.min || double > data.max) { + throw SerializationException("Number is out of range: $value, expected ${data.min}..${data.max}") + } + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt new file mode 100644 index 000000000..37d220c31 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class StartsWith(val prefix: String) { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: StartsWith, type: Type): Constraint = { value -> + if (value != null && !value.startsWith(data.prefix)) { + throw SerializationException("String must start with '${data.prefix}'") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Trimmed.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Trimmed.kt new file mode 100644 index 000000000..90e28d8eb --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Trimmed.kt @@ -0,0 +1,30 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type + +/** + * Requires a string config value to have no leading or trailing whitespace. + * + * This validates only; it does not mutate the loaded value. + * + * Usage: + * ```kotlin + * @field:Trimmed + * val id: String = "example" + * ``` + */ +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class Trimmed { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: Trimmed, type: Type): Constraint = { value -> + if (value != null && value != value.trim()) { + throw SerializationException("String must not have leading or trailing whitespace") + } + } + } + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt new file mode 100644 index 000000000..a7a8c7566 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.constraints + +import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.Type +import kotlin.io.path.exists +import kotlin.io.path.isWritable + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +annotation class WritablePath { + companion object { + internal object Factory : Constraint.Factory { + override fun make(data: WritablePath, type: Type): Constraint = Constraint { value -> + val path = value.asPathOrNull() ?: return@Constraint + if (path.exists() && !path.isWritable()) { + throw SerializationException("Path must be writable: $path") + } + } + } + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/FileSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/FileSerializer.kt new file mode 100644 index 000000000..3407c871d --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/FileSerializer.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import java.io.File +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object FileSerializer : ScalarSerializer.Annotated(File::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): File { + return File(obj.toString()) + } + + override fun serialize(type: AnnotatedType, item: File, typeSupported: Predicate>): Any { + return item.path + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt new file mode 100644 index 000000000..f16a4d4d2 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import java.lang.reflect.AnnotatedType +import java.nio.file.Path +import java.util.function.Predicate + +internal object PathSerializer : ScalarSerializer.Annotated(Path::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Path { + return Path.of(obj.toString()) + } + + override fun serialize(type: AnnotatedType, item: Path, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PatternSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PatternSerializer.kt new file mode 100644 index 000000000..13559dc41 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PatternSerializer.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +internal object PatternSerializer : ScalarSerializer.Annotated(Pattern::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Pattern { + return try { + Pattern.compile(obj.toString()) + } catch (e: PatternSyntaxException) { + throw SerializationException(Pattern::class.java, "$obj($type) is not a valid pattern", e) + } + } + + override fun serialize(type: AnnotatedType, item: Pattern, typeSupported: Predicate>): Any { + return item.pattern() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/RegexSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/RegexSerializer.kt new file mode 100644 index 000000000..9811fb997 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/RegexSerializer.kt @@ -0,0 +1,21 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import java.util.regex.PatternSyntaxException + +internal object RegexSerializer : ScalarSerializer.Annotated(Regex::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Regex { + return try { + obj.toString().toRegex() + } catch (e: PatternSyntaxException) { + throw SerializationException(Regex::class.java, "$obj($type) is not a valid regex", e) + } + } + + override fun serialize(type: AnnotatedType, item: Regex, typeSupported: Predicate>): Any { + return item.pattern + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt index 5333619e7..96e18ef5a 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt @@ -1,8 +1,6 @@ package dev.slne.surf.api.core.config.serializer -import dev.slne.surf.api.core.config.constraints.MaxNumber -import dev.slne.surf.api.core.config.constraints.MinNumber -import dev.slne.surf.api.core.config.constraints.PositiveNumber +import dev.slne.surf.api.core.config.constraints.* import dev.slne.surf.api.core.config.serializer.collection.map.FastutilMapSerializer import dev.slne.surf.api.core.config.serializer.collection.map.MapSerializer import dev.slne.surf.api.core.config.type.BooleanOrDefault @@ -18,6 +16,7 @@ import it.unimi.dsi.fastutil.ints.* import it.unimi.dsi.fastutil.longs.* import it.unimi.dsi.fastutil.objects.* import net.kyori.adventure.text.Component +import org.jetbrains.annotations.MustBeInvokedByOverriders import org.spongepowered.configurate.kotlin.dataClassFieldDiscoverer import org.spongepowered.configurate.kotlin.extensions.addConstraint import org.spongepowered.configurate.objectmapping.ObjectMapper @@ -65,6 +64,10 @@ abstract class SpongeConfigSerializers { _typeTokenSerializers.remove(typeToken) } + @MustBeInvokedByOverriders + protected open fun registerDefaults(builder: TypeSerializerCollection.Builder) { + } + /** * Registers custom serializers with the provided builder. */ @@ -89,6 +92,14 @@ abstract class SpongeConfigSerializers { builder.register(DoubleOr.Default.Serializer) builder.register(DoubleOr.Disabled.Serializer) builder.register(MapSerializer.TYPE, MapSerializer(false)) + builder.register(UuidSerializer) + builder.register(RegexSerializer) + builder.register(PatternSerializer) + builder.register(UriSerializer) + builder.register(UrlSerializer) + builder.register(PathSerializer) + builder.register(FileSerializer) + builder.register(TextColorSerializer) //region fastutil maps // @formatter:off @@ -138,10 +149,32 @@ abstract class SpongeConfigSerializers { ObjectMapper.factoryBuilder() .addDiscoverer(dataClassFieldDiscoverer()) .addConstraint(PositiveNumber.Companion.Factory) + .addConstraint(NegativeNumber.Companion.Factory) .addConstraint(MinNumber.Companion.Factory) .addConstraint(MaxNumber.Companion.Factory) + .addConstraint(NotBlank.Companion.Factory) + .addConstraint(Trimmed.Companion.Factory) + .addConstraint(MaxLength.Companion.Factory) + .addConstraint(MinLength.Companion.Factory) + .addConstraint(StartsWith.Companion.Factory) + .addConstraint(EndsWith.Companion.Factory) + .addConstraint(Contains.Companion.Factory) + .addConstraint(Range.Companion.Factory) + .addConstraint(MinSize.Companion.Factory) + .addConstraint(MaxSize.Companion.Factory) + .addConstraint(NotEmpty.Companion.Factory) + .addConstraint(NoDuplicates.Companion.Factory) + .addConstraint(MinDuration.Companion.Factory) + .addConstraint(MaxDuration.Companion.Factory) + .addConstraint(DisallowValues.Companion.Factory) + .addConstraint(Namespace.Companion.Factory) + .addConstraint(ExistingFile.Companion.Factory) + .addConstraint(Directory.Companion.Factory) + .addConstraint(WritablePath.Companion.Factory) .build() ) + + registerDefaults(builder) } /** diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/TextColorSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/TextColorSerializer.kt new file mode 100644 index 000000000..9b056b15b --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/TextColorSerializer.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.api.core.config.serializer + +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextColor +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object TextColorSerializer : ScalarSerializer.Annotated(TextColor::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): TextColor { + val input = obj.toString().trim() + + NamedTextColor.NAMES.value(input)?.let { return it } + TextColor.fromHexString(input)?.let { return it } + + val rgb = input.split(',', ';').map { it.trim() } + if (rgb.size == 3) { + val red = rgb[0].toIntOrNull() + val green = rgb[1].toIntOrNull() + val blue = rgb[2].toIntOrNull() + + if (red != null && green != null && blue != null) { + return TextColor.color(red, green, blue) + } + } + + throw SerializationException(TextColor::class.java, "$obj($type) is not a valid text color") + } + + override fun serialize(type: AnnotatedType, item: TextColor, typeSupported: Predicate>): Any { + return NamedTextColor.namedColor(item.value())?.toString() ?: item.asHexString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UriSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UriSerializer.kt new file mode 100644 index 000000000..f9b134180 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UriSerializer.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.net.URI +import java.net.URISyntaxException +import java.util.function.Predicate + +internal object UriSerializer : ScalarSerializer.Annotated(URI::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): URI { + return try { + URI(obj.toString()) + } catch (e: URISyntaxException) { + throw SerializationException(URI::class.java, "$obj($type) is not a valid URI", e) + } + } + + override fun serialize(type: AnnotatedType, item: URI, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UrlSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UrlSerializer.kt new file mode 100644 index 000000000..53e5bdcf8 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UrlSerializer.kt @@ -0,0 +1,23 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.net.MalformedURLException +import java.net.URL +import java.util.function.Predicate + +@Suppress("DEPRECATION") +internal object UrlSerializer : ScalarSerializer.Annotated(URL::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): URL { + return try { + URL(obj.toString()) + } catch (e: MalformedURLException) { + throw SerializationException(URL::class.java, "$obj($type) is not a valid URL", e) + } + } + + override fun serialize(type: AnnotatedType, item: URL, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UuidSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UuidSerializer.kt new file mode 100644 index 000000000..b50d07867 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/UuidSerializer.kt @@ -0,0 +1,21 @@ +package dev.slne.surf.api.core.config.serializer + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.* +import java.util.function.Predicate + +internal object UuidSerializer : ScalarSerializer.Annotated(UUID::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): UUID { + return try { + UUID.fromString(obj.toString()) + } catch (e: IllegalArgumentException) { + throw SerializationException(UUID::class.java, "$obj($type) is not a valid UUID", e) + } + } + + override fun serialize(type: AnnotatedType, item: UUID, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/BukkitConfigurationSerializableSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/BukkitConfigurationSerializableSerializer.kt new file mode 100644 index 000000000..7218ef029 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/BukkitConfigurationSerializableSerializer.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import io.leangen.geantyref.GenericTypeReflector +import org.bukkit.configuration.serialization.ConfigurationSerializable +import org.bukkit.configuration.serialization.ConfigurationSerialization +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer +import java.lang.reflect.Type + +object BukkitConfigurationSerializableSerializer : TypeSerializer { + @Suppress("UNCHECKED_CAST") + override fun deserialize(type: Type, node: ConfigurationNode): ConfigurationSerializable { + val clazz = GenericTypeReflector.erase(type) as Class + val args = node.childrenMap().mapValues { it.value.raw() }.mapKeys { it.key.toString() } + + try { + return ConfigurationSerialization.deserializeObject(args, clazz) + ?: throw SerializationException( + clazz, + "Could not deserialize ${clazz.name} via ConfigurationSerializable" + ) + } catch (e: Throwable) { + if (e is SerializationException) throw e + throw SerializationException(clazz, "Could not deserialize ${clazz.name} via ConfigurationSerializable", e) + } + } + + + @Suppress("OverrideOnly") + override fun serialize(type: Type, obj: ConfigurationSerializable?, node: ConfigurationNode) { + if (obj == null) { + node.raw(null) + return + } + + val map = obj.serialize() + map.forEach { (key, value) -> + node.node(key).set(value) + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt new file mode 100644 index 000000000..cf3b2905c --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt @@ -0,0 +1,23 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import org.bukkit.inventory.ItemStack +import org.spongepowered.configurate.serialize.ScalarSerializer +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate +import kotlin.io.encoding.Base64 + +object ItemStackSerializer : ScalarSerializer.Annotated(ItemStack::class.java) { + + override fun deserialize(type: AnnotatedType, obj: Any): ItemStack { + val base64 = obj.toString() + val bytes = Base64.decode(base64) + + return ItemStack.deserializeBytes(bytes) + } + + override fun serialize(type: AnnotatedType, item: ItemStack?, typeSupported: Predicate>): Any? { + if (item == null) return null + val serialized = item.serializeAsBytes() + return Base64.encode(serialized) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt new file mode 100644 index 000000000..4644a9364 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt @@ -0,0 +1,42 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import net.kyori.adventure.key.Key +import org.bukkit.Bukkit +import org.bukkit.Location +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.SerializationException +import org.spongepowered.configurate.serialize.TypeSerializer +import java.lang.reflect.Type + +internal object LocationSerializer : TypeSerializer { + override fun deserialize(type: Type, node: ConfigurationNode): Location { + val worldKey = node.node("world-key").get(Key::class.java) + ?: throw SerializationException(Location::class.java, "Location is missing world") + + val world = Bukkit.getWorld(worldKey) + ?: throw SerializationException(Location::class.java, "Unknown world: $worldKey") + + return Location( + world, + node.node("x").double, + node.node("y").double, + node.node("z").double, + node.node("yaw").float, + node.node("pitch").float + ) + } + + override fun serialize(type: Type, obj: Location?, node: ConfigurationNode) { + if (obj == null) { + node.raw(null) + return + } + + node.node("world").set(obj.world.key) + node.node("x").set(obj.x) + node.node("y").set(obj.y) + node.node("z").set(obj.z) + node.node("yaw").set(obj.yaw) + node.node("pitch").set(obj.pitch) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/MaterialSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/MaterialSerializer.kt new file mode 100644 index 000000000..4be6c448e --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/MaterialSerializer.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import org.bukkit.Material +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object MaterialSerializer : ScalarSerializer.Annotated(Material::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): Material { + return Material.matchMaterial(obj.toString()) ?: throw SerializationException( + Material::class.java, + "$obj($type) is not a valid material" + ) + } + + override fun serialize(type: AnnotatedType, item: Material, typeSupported: Predicate>): Any { + return item.key.toString() + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/NamespacedKeySerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/NamespacedKeySerializer.kt new file mode 100644 index 000000000..541dd5fb2 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/NamespacedKeySerializer.kt @@ -0,0 +1,18 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import org.bukkit.NamespacedKey +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +internal object NamespacedKeySerializer : ScalarSerializer.Annotated(NamespacedKey::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): NamespacedKey { + return NamespacedKey.fromString(obj.toString()) + ?: throw SerializationException(NamespacedKey::class.java, "$obj($type) is not a valid NamespacedKey") + } + + override fun serialize(type: AnnotatedType, item: NamespacedKey, typeSupported: Predicate>): Any { + return item.toString() + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt index 64df59d53..52ec969cf 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt @@ -2,45 +2,95 @@ package dev.slne.surf.api.paper.server.config.serializers import com.google.auto.service.AutoService import dev.slne.surf.api.core.config.serializer.SpongeConfigSerializers -import org.bukkit.inventory.ItemStack -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.serialize.SerializationException -import org.spongepowered.configurate.serialize.TypeSerializer -import java.lang.reflect.Type -import java.util.* +import dev.slne.surf.api.paper.server.PaperInstance +import dev.slne.surf.api.paper.server.config.serializers.registry.RegistryValueSerializer +import io.papermc.paper.datacomponent.DataComponentType +import io.papermc.paper.dialog.Dialog +import io.papermc.paper.entity.poi.PoiType +import io.papermc.paper.registry.RegistryKey +import org.bukkit.* +import org.bukkit.attribute.Attribute +import org.bukkit.block.Biome +import org.bukkit.block.BlockType +import org.bukkit.block.banner.PatternType +import org.bukkit.configuration.serialization.ConfigurationSerializable +import org.bukkit.damage.DamageType +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.* +import org.bukkit.generator.structure.Structure +import org.bukkit.generator.structure.StructureType +import org.bukkit.inventory.ItemType +import org.bukkit.inventory.MenuType +import org.bukkit.inventory.meta.trim.TrimMaterial +import org.bukkit.inventory.meta.trim.TrimPattern +import org.bukkit.map.MapCursor +import org.bukkit.potion.PotionEffectType +import org.spongepowered.configurate.serialize.TypeSerializerCollection +@Suppress("UnstableApiUsage") @AutoService(SpongeConfigSerializers::class) class PaperSpongeConfigSerializers : SpongeConfigSerializers() { - init { - registerClassSerializer(ItemStackSerializer) - } - object ItemStackSerializer : TypeSerializer { - override fun deserialize( - type: Type, - node: ConfigurationNode - ): ItemStack { - val itemStackBase64 = node.string - ?: throw SerializationException("Expected a Base64 string for ItemStack deserialization") - val decoded = Base64.getDecoder().decode(itemStackBase64) + override fun registerDefaults(builder: TypeSerializerCollection.Builder) { + super.registerDefaults(builder) - return ItemStack.deserializeBytes(decoded) - } + builder.register(NamespacedKeySerializer) + builder.register(MaterialSerializer) +// builder.register(Vector::class.java, VectorSerializer) + builder.register(Location::class.java, LocationSerializer) - override fun serialize( - type: Type, - obj: ItemStack?, - node: ConfigurationNode - ) { - if (obj == null) { - node.raw(null) - return - } - val serialized = obj.serializeAsBytes() - val encoded = Base64.getEncoder().encodeToString(serialized) + if (!PaperInstance.isBootstrapping()) { + builder.register(ItemStackSerializer) - node.set(encoded) + //region Registry serializer + //@formatter:off + builder.register(RegistryValueSerializer(GameEvent::class.java, RegistryKey.GAME_EVENT, true)) + builder.register(RegistryValueSerializer(StructureType::class.java, RegistryKey.STRUCTURE_TYPE, true)) + builder.register(RegistryValueSerializer(PotionEffectType::class.java, RegistryKey.MOB_EFFECT, true)) + builder.register(RegistryValueSerializer(BlockType::class.java, RegistryKey.BLOCK, true)) + builder.register(RegistryValueSerializer(ItemType::class.java, RegistryKey.ITEM, true)) + builder.register(RegistryValueSerializer(Villager.Profession::class.java, RegistryKey.VILLAGER_PROFESSION, true)) + builder.register(RegistryValueSerializer(PoiType::class.java, RegistryKey.POINT_OF_INTEREST_TYPE, true)) + builder.register(RegistryValueSerializer(Villager.Type::class.java, RegistryKey.VILLAGER_TYPE, true)) + builder.register(RegistryValueSerializer(MapCursor.Type::class.java, RegistryKey.MAP_DECORATION_TYPE, true)) + builder.register(RegistryValueSerializer(MenuType::class.java, RegistryKey.MENU, true)) + builder.register(RegistryValueSerializer(Attribute::class.java, RegistryKey.ATTRIBUTE, true)) + builder.register(RegistryValueSerializer(Fluid::class.java, RegistryKey.FLUID, true)) + builder.register(RegistryValueSerializer(Sound::class.java, RegistryKey.SOUND_EVENT, true)) + builder.register(RegistryValueSerializer(DataComponentType::class.java, RegistryKey.DATA_COMPONENT_TYPE, true)) + builder.register(RegistryValueSerializer(GameRule::class.java, RegistryKey.GAME_RULE, true)) + builder.register(RegistryValueSerializer(Biome::class.java, RegistryKey.BIOME, true)) + builder.register(RegistryValueSerializer(Structure::class.java, RegistryKey.STRUCTURE, true)) + builder.register(RegistryValueSerializer(TrimMaterial::class.java, RegistryKey.TRIM_MATERIAL, true)) + builder.register(RegistryValueSerializer(TrimPattern::class.java, RegistryKey.TRIM_PATTERN, true)) + builder.register(RegistryValueSerializer(DamageType::class.java, RegistryKey.DAMAGE_TYPE, true)) + builder.register(RegistryValueSerializer(Wolf.Variant::class.java, RegistryKey.WOLF_VARIANT, true)) + builder.register(RegistryValueSerializer(Wolf.SoundVariant::class.java, RegistryKey.WOLF_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Enchantment::class.java, RegistryKey.ENCHANTMENT, true)) + builder.register(RegistryValueSerializer(JukeboxSong::class.java, RegistryKey.JUKEBOX_SONG, true)) + builder.register(RegistryValueSerializer(PatternType::class.java, RegistryKey.BANNER_PATTERN, true)) + builder.register(RegistryValueSerializer(Art::class.java, RegistryKey.PAINTING_VARIANT, true)) + builder.register(RegistryValueSerializer(MusicInstrument::class.java, RegistryKey.INSTRUMENT, true)) + builder.register(RegistryValueSerializer(Cat.Type::class.java, RegistryKey.CAT_VARIANT, true)) + builder.register(RegistryValueSerializer(Cat.SoundVariant::class.java, RegistryKey.CAT_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Frog.Variant::class.java, RegistryKey.FROG_VARIANT, true)) + builder.register(RegistryValueSerializer(Chicken.Variant::class.java, RegistryKey.CHICKEN_VARIANT, true)) + builder.register(RegistryValueSerializer(Chicken.SoundVariant::class.java, RegistryKey.CHICKEN_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Cow.Variant::class.java, RegistryKey.COW_VARIANT, true)) + builder.register(RegistryValueSerializer(Cow.SoundVariant::class.java, RegistryKey.COW_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(Pig.Variant::class.java, RegistryKey.PIG_VARIANT, true)) + builder.register(RegistryValueSerializer(Pig.SoundVariant::class.java, RegistryKey.PIG_SOUND_VARIANT, true)) + builder.register(RegistryValueSerializer(ZombieNautilus.Variant::class.java, RegistryKey.ZOMBIE_NAUTILUS_VARIANT, true)) + builder.register(RegistryValueSerializer(Dialog::class.java, RegistryKey.DIALOG, true)) + //@formatter:on + //endregion } + + // register last to let other serializer override this + builder.register( + { type -> type is Class<*> && ConfigurationSerializable::class.java.isAssignableFrom(type) }, + BukkitConfigurationSerializableSerializer + ) } } \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt new file mode 100644 index 000000000..b1bd3780b --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt @@ -0,0 +1,27 @@ +package dev.slne.surf.api.paper.server.config.serializers + +import org.bukkit.util.Vector +import org.spongepowered.configurate.ConfigurationNode +import org.spongepowered.configurate.serialize.TypeSerializer +import java.lang.reflect.Type + +internal object VectorSerializer : TypeSerializer { + override fun deserialize(type: Type, node: ConfigurationNode): Vector { + return Vector( + node.node("x").double, + node.node("y").double, + node.node("z").double + ) + } + + override fun serialize(type: Type, obj: Vector?, node: ConfigurationNode) { + if (obj == null) { + node.raw(null) + return + } + + node.node("x").set(obj.x) + node.node("y").set(obj.y) + node.node("z").set(obj.z) + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt new file mode 100644 index 000000000..a573a24ae --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt @@ -0,0 +1,66 @@ +package dev.slne.surf.api.paper.server.config.serializers.registry + +import io.leangen.geantyref.TypeToken +import io.papermc.paper.registry.RegistryAccess +import io.papermc.paper.registry.RegistryKey +import net.kyori.adventure.key.InvalidKeyException +import net.kyori.adventure.key.Key +import org.bukkit.Keyed +import org.bukkit.Registry +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.util.function.Predicate + +abstract class RegistryEntrySerializer : ScalarSerializer.Annotated { + + private val registryKey: RegistryKey + private val omitMinecraftNamespace: Boolean + + protected constructor( + type: TypeToken, + registryKey: RegistryKey, + omitMinecraftNamespace: Boolean + ) : super(type) { + this.registryKey = registryKey + this.omitMinecraftNamespace = omitMinecraftNamespace + } + + protected constructor( + type: Class, + registryKey: RegistryKey, + omitMinecraftNamespace: Boolean + ) : super(type) { + this.registryKey = registryKey + this.omitMinecraftNamespace = omitMinecraftNamespace + } + + protected fun registry(): Registry { + return RegistryAccess.registryAccess().getRegistry(registryKey) + } + + protected abstract fun convertFromResourceKey(key: Key): T + + override fun deserialize(type: AnnotatedType?, obj: Any): T? { + return convertFromResourceKey(deserializeKey(obj)) + } + + protected abstract fun convertToResourceKey(value: T): Key + + override fun serialize(type: AnnotatedType, item: T, typeSupported: Predicate>): Any { + val key = this.convertToResourceKey(item) + return if (this.omitMinecraftNamespace && key.key().namespace() == Key.MINECRAFT_NAMESPACE) { + key.key().value() + } else { + key.asString() + } + } + + private fun deserializeKey(input: Any): Key { + return try { + Key.key(input.toString()) + } catch (e: InvalidKeyException) { + throw SerializationException(Key::class.java, "Could not create a key from input", e) + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt new file mode 100644 index 000000000..a117febb2 --- /dev/null +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt @@ -0,0 +1,32 @@ +package dev.slne.surf.api.paper.server.config.serializers.registry + +import io.leangen.geantyref.TypeToken +import io.papermc.paper.registry.RegistryKey +import net.kyori.adventure.key.Key +import org.bukkit.Keyed +import org.spongepowered.configurate.serialize.SerializationException + +class RegistryValueSerializer : RegistryEntrySerializer { + constructor(type: TypeToken, registryKey: RegistryKey, omitMinecraftNamespace: Boolean) : super( + type, + registryKey, + omitMinecraftNamespace + ) + + constructor(type: Class, registryKey: RegistryKey, omitMinecraftNamespace: Boolean) : super( + type, + registryKey, + omitMinecraftNamespace + ) + + override fun convertFromResourceKey(key: Key): T { + val value = registry().get(key) + ?: throw SerializationException("Missing value in ${registry()} with string key: ${key.asString()}") + + return value + } + + override fun convertToResourceKey(value: T): Key { + return registry().getKeyOrThrow(value) + } +} \ No newline at end of file From 637d9cf90c8ea260b07aaee4358317c6964b38df Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 22:44:31 +0200 Subject: [PATCH 05/18] =?UTF-8?q?=E2=9C=A8=20feat(config):=20add=20ConfigD?= =?UTF-8?q?uration=20type=20and=20update=20duration=20constraints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce ConfigDuration data class for handling duration values - implement parsing and formatting for duration strings - update DurationOrDisabled and duration constraints to use ConfigDuration - add ModernSerializerTestConfig for testing new configuration types --- .../core/config/constraints/MaxDuration.kt | 10 +- .../core/config/constraints/MinDuration.kt | 10 +- .../config/serializer/DurationSerializer.kt | 64 ------- .../serializer/SpongeConfigSerializers.kt | 53 +++--- .../api/core/config/type/ConfigDuration.kt | 76 ++++++++ .../core/config/type/DurationOrDisabled.kt | 5 +- .../test/command/SurfApiTestCommand.java | 5 +- .../surfapi/bukkit/test/PaperPluginMain.kt | 4 +- .../ModernSerializerTestConfigCommand.kt | 17 ++ .../test/config/ModernSerializerTestConfig.kt | 163 ++++++++++++++++++ .../PaperSpongeConfigSerializers.kt | 3 +- 11 files changed, 302 insertions(+), 108 deletions(-) delete mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt create mode 100644 surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt create mode 100644 surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/ModernSerializerTestConfigCommand.kt create mode 100644 surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt index 80dec99c5..81a4d422f 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt @@ -1,18 +1,18 @@ package dev.slne.surf.api.core.config.constraints +import dev.slne.surf.api.core.config.type.ConfigDuration import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type -import kotlin.time.Duration @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class MaxDuration(val seconds: Long) { companion object { - internal object Factory : Constraint.Factory { - override fun make(data: MaxDuration, type: Type): Constraint = { value -> - if (value != null && value.inWholeSeconds > data.seconds) { - throw SerializationException("Duration is too long: $value, expected <= ${data.seconds}s") + internal object Factory : Constraint.Factory { + override fun make(data: MaxDuration, type: Type): Constraint = { value -> + if (value != null && value.value.inWholeSeconds > data.seconds) { + throw SerializationException("Duration is too long: ${value.value}, expected <= ${data.seconds}s") } } } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt index 12145717e..2aa420ab4 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt @@ -1,18 +1,18 @@ package dev.slne.surf.api.core.config.constraints +import dev.slne.surf.api.core.config.type.ConfigDuration import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type -import kotlin.time.Duration @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class MinDuration(val seconds: Long) { companion object { - internal object Factory : Constraint.Factory { - override fun make(data: MinDuration, type: Type): Constraint = { value -> - if (value != null && value.inWholeSeconds < data.seconds) { - throw SerializationException("Duration is too short: $value, expected >= ${data.seconds}s") + internal object Factory : Constraint.Factory { + override fun make(data: MinDuration, type: Type): Constraint = { value -> + if (value != null && value.value.inWholeSeconds < data.seconds) { + throw SerializationException("Duration is too short: ${value.value}, expected >= ${data.seconds}s") } } } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt deleted file mode 100644 index 9f73e597f..000000000 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/DurationSerializer.kt +++ /dev/null @@ -1,64 +0,0 @@ -package dev.slne.surf.api.core.config.serializer - -import org.spongepowered.configurate.serialize.ScalarSerializer -import org.spongepowered.configurate.serialize.SerializationException -import java.lang.reflect.AnnotatedType -import java.math.BigDecimal -import java.util.function.Predicate -import kotlin.time.Duration -import kotlin.time.DurationUnit -import kotlin.time.toDuration - -/** - * Configurate scalar serializer for Kotlin [Duration] values. - * - * Supported configuration formats: - * - `10s` - * - `5m` - * - `2h` - * - `1d` - * - * Serialized durations are written back as seconds. - */ -internal object DurationSerializer : ScalarSerializer.Annotated(Duration::class.java) { - private val DURATION_PATTERN = Regex("""^\s*(-?\d+(?:\.\d+)?)\s*([dhms])\s*$""", RegexOption.IGNORE_CASE) - - /** - * Parses a duration string into a Kotlin [Duration]. - */ - override fun deserialize(type: AnnotatedType, obj: Any): Duration { - val value = obj.toString() - val match = DURATION_PATTERN.matchEntire(value) - ?: throw SerializationException(Duration::class.java, "$obj($type) is not a duration") - - val amount = match.groupValues[1].toDoubleOrNull() - ?: throw SerializationException(Duration::class.java, "$obj($type) is not a duration") - - val unit = when (match.groupValues[2].lowercase()) { - "d" -> DurationUnit.DAYS - "h" -> DurationUnit.HOURS - "m" -> DurationUnit.MINUTES - "s" -> DurationUnit.SECONDS - else -> throw SerializationException(Duration::class.java, "$obj($type) is not a duration") - } - - return amount.toDuration(unit) - } - - /** - * Serializes a finite Kotlin [Duration] as a seconds-based duration string. - * - * @throws SerializationException if [item] is infinite. - */ - public override fun serialize(type: AnnotatedType, item: Duration, typeSupported: Predicate>): Any { - if (item.isInfinite()) { - throw SerializationException(Duration::class.java, "$item($type) is infinite and cannot be serialized") - } - - return "${formatNumber(item.toDouble(DurationUnit.SECONDS))}s" - } - - private fun formatNumber(value: Double): String { - return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString() - } -} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt index 96e18ef5a..a4b6237a2 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt @@ -4,6 +4,7 @@ import dev.slne.surf.api.core.config.constraints.* import dev.slne.surf.api.core.config.serializer.collection.map.FastutilMapSerializer import dev.slne.surf.api.core.config.serializer.collection.map.MapSerializer import dev.slne.surf.api.core.config.type.BooleanOrDefault +import dev.slne.surf.api.core.config.type.ConfigDuration import dev.slne.surf.api.core.config.type.DurationOrDisabled import dev.slne.surf.api.core.config.type.number.DoubleOr import dev.slne.surf.api.core.config.type.number.IntOr @@ -84,14 +85,13 @@ abstract class SpongeConfigSerializers { builder.register(ComponentSerializer()) builder.register(EnumValueSerializer) builder.register(KeySerializer) - builder.register(DurationSerializer) + builder.register(ConfigDuration.Serializer) builder.register(BooleanOrDefault.Serializer) builder.register(DurationOrDisabled.Serializer) builder.register(IntOr.Default.Serializer) builder.register(IntOr.Disabled.Serializer) builder.register(DoubleOr.Default.Serializer) builder.register(DoubleOr.Disabled.Serializer) - builder.register(MapSerializer.TYPE, MapSerializer(false)) builder.register(UuidSerializer) builder.register(RegexSerializer) builder.register(PatternSerializer) @@ -103,15 +103,15 @@ abstract class SpongeConfigSerializers { //region fastutil maps // @formatter:off - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2BooleanOpenHashMap(it as Map) }, java.lang.Boolean.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ByteOpenHashMap(it as Map) }, java.lang.Byte.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2CharOpenHashMap(it as Map) }, java.lang.Character.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2DoubleOpenHashMap(it as Map) }, java.lang.Double.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2FloatOpenHashMap(it as Map) }, java.lang.Float.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2IntOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2LongOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ShortOpenHashMap(it as Map) }, java.lang.Short.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Reference2ObjectOpenHashMap(it) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2BooleanOpenHashMap(it as Map<*, Boolean>) }, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ByteOpenHashMap(it as Map<*, Byte>) }, java.lang.Byte.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2CharOpenHashMap(it as Map<*, Char>) }, java.lang.Character.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2DoubleOpenHashMap(it as Map<*, Double>) }, java.lang.Double.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2FloatOpenHashMap(it as Map<*, Float>) }, java.lang.Float.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2IntOpenHashMap(it as Map<*, Int>) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2LongOpenHashMap(it as Map<*, Long>) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ShortOpenHashMap(it as Map<*, Short>) }, java.lang.Short.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Reference2ObjectOpenHashMap(it) })) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2BooleanOpenHashMap(it as Map) }, Integer.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2ByteOpenHashMap(it as Map) }, Integer.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2CharOpenHashMap(it as Map) }, Integer.TYPE)) @@ -119,8 +119,8 @@ abstract class SpongeConfigSerializers { builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2FloatOpenHashMap(it as Map) }, Integer.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.SomethingToSomething({ Int2IntOpenHashMap(it as Map) })) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2LongOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ObjectOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ReferenceOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ObjectOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ReferenceOpenHashMap(it as Map) }, Integer.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2ShortOpenHashMap(it as Map) }, Integer.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2BooleanOpenHashMap(it as Map) }, java.lang.Long.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2ByteOpenHashMap(it as Map) }, java.lang.Long.TYPE)) @@ -129,22 +129,25 @@ abstract class SpongeConfigSerializers { builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2FloatOpenHashMap(it as Map) }, java.lang.Long.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2IntOpenHashMap(it as Map) }, java.lang.Long.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.SomethingToSomething({ Long2LongOpenHashMap(it as Map) })) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ObjectOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ReferenceOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ObjectOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ReferenceOpenHashMap(it as Map) }, java.lang.Long.TYPE)) builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2ShortOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2BooleanOpenHashMap(it as Map) }, java.lang.Boolean.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ByteOpenHashMap(it as Map) }, java.lang.Byte.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2CharOpenHashMap(it as Map) }, java.lang.Character.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2DoubleOpenHashMap(it as Map) }, java.lang.Double.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2FloatOpenHashMap(it as Map) }, java.lang.Float.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2IntOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2LongOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ObjectOpenHashMap(it) })) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ReferenceOpenHashMap(it) })) - builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ShortOpenHashMap(it as Map) }, java.lang.Short.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2BooleanOpenHashMap(it as Map<*, Boolean>) }, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ByteOpenHashMap(it as Map<*, Byte>) }, java.lang.Byte.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2CharOpenHashMap(it as Map<*, Char>) }, java.lang.Character.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2DoubleOpenHashMap(it as Map<*, Double>) }, java.lang.Double.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2FloatOpenHashMap(it as Map<*, Float>) }, java.lang.Float.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2IntOpenHashMap(it as Map<*, Int>) }, Integer.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2LongOpenHashMap(it as Map<*, Long>) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ObjectOpenHashMap(it) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Object2ReferenceOpenHashMap(it) })) + builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ShortOpenHashMap(it as Map<*, Short>) }, java.lang.Short.TYPE)) // @formatter:on //endregion + // register after fastutil specific serializers have been registered + builder.register(MapSerializer.TYPE, MapSerializer(false)) + builder.registerAnnotatedObjects( ObjectMapper.factoryBuilder() .addDiscoverer(dataClassFieldDiscoverer()) diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt new file mode 100644 index 000000000..3e90e0f96 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt @@ -0,0 +1,76 @@ +package dev.slne.surf.api.core.config.type + +import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException +import java.lang.reflect.AnnotatedType +import java.math.BigDecimal +import java.util.function.Predicate +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +data class ConfigDuration( + val value: Duration, + val rawValue: String = format(value) +) { + fun asDuration(): Duration = value + fun inWholeSeconds(): Long = value.inWholeSeconds + + companion object { + private val DURATION_PATTERN = + Regex("""^\s*(-?\d+(?:\.\d+)?)\s*([dhms])\s*$""", RegexOption.IGNORE_CASE) + + fun parse(input: String): ConfigDuration { + val cleaned = input.trim() + val match = DURATION_PATTERN.matchEntire(cleaned) + ?: throw SerializationException(ConfigDuration::class.java, "$input is not a duration") + + val amount = match.groupValues[1].toDoubleOrNull() + ?: throw SerializationException(ConfigDuration::class.java, "$input is not a duration") + + val unit = when (match.groupValues[2].lowercase()) { + "d" -> DurationUnit.DAYS + "h" -> DurationUnit.HOURS + "m" -> DurationUnit.MINUTES + "s" -> DurationUnit.SECONDS + else -> throw SerializationException(ConfigDuration::class.java, "$input is not a duration") + } + + return ConfigDuration(amount.toDuration(unit), cleaned) + } + + fun format(duration: Duration): String { + if (duration.isInfinite()) { + throw SerializationException( + ConfigDuration::class.java, + "$duration is infinite and cannot be serialized" + ) + } + + return when (duration) { + duration.inWholeDays.toDuration(DurationUnit.DAYS) -> "${duration.inWholeDays}d" + duration.inWholeHours.toDuration(DurationUnit.HOURS) -> "${duration.inWholeHours}h" + duration.inWholeMinutes.toDuration(DurationUnit.MINUTES) -> "${duration.inWholeMinutes}m" + else -> "${duration.toDouble(DurationUnit.SECONDS).formatNumber()}s" + } + } + } + + internal object Serializer : ScalarSerializer.Annotated(ConfigDuration::class.java) { + override fun deserialize(type: AnnotatedType, obj: Any): ConfigDuration { + return parse(obj.toString()) + } + + public override fun serialize( + type: AnnotatedType, + item: ConfigDuration, + typeSupported: Predicate> + ): Any { + return item.rawValue + } + } +} + +private fun Double.formatNumber(): String { + return BigDecimal.valueOf(this).stripTrailingZeros().toPlainString() +} \ No newline at end of file diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt index 484ad80f9..bc1c9c145 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/DurationOrDisabled.kt @@ -1,6 +1,5 @@ package dev.slne.surf.api.core.config.type -import dev.slne.surf.api.core.config.serializer.DurationSerializer import org.spongepowered.configurate.serialize.ScalarSerializer import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.AnnotatedType @@ -67,7 +66,7 @@ data class DurationOrDisabled private constructor(val value: Duration?) { return try { DurationOrDisabled( - DurationSerializer.deserialize(type, obj) + ConfigDuration.Serializer.deserialize(type, obj).asDuration() ) } catch (e: Exception) { throw SerializationException( @@ -89,7 +88,7 @@ data class DurationOrDisabled private constructor(val value: Duration?) { item: DurationOrDisabled, typeSupported: Predicate> ): Any { - return item.value?.let { DurationSerializer.serialize(type, it, typeSupported) } ?: DISABLED_VALUE + return item.value?.let { ConfigDuration.Serializer.serialize(type, ConfigDuration(it), typeSupported) } ?: DISABLED_VALUE } } } \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java index d61c79347..c4dcc9208 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/java/dev/slne/surf/api/paper/test/command/SurfApiTestCommand.java @@ -1,6 +1,6 @@ package dev.slne.surf.api.paper.test.command; -import dev.jorel.commandapi.*; +import dev.jorel.commandapi.CommandAPICommand; import dev.slne.surf.api.paper.test.command.subcommands.*; import dev.slne.surf.surfapi.bukkit.test.command.subcommands.*; @@ -32,7 +32,8 @@ public SurfApiTestCommand() { new SortInvCommand("sortInv"), new SignedMessageArgumentTest("signedmessage"), new BlockPdcContainerTest("blockpdc"), - new OfflineInventoryEditTest("editOfflineInventory") + new OfflineInventoryEditTest("editOfflineInventory"), + new ModernSerializerTestConfigCommand("modernSerializerTestConfig") ); } } diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt index c36793593..2e7064a77 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/PaperPluginMain.kt @@ -5,14 +5,12 @@ import com.destroystokyo.paper.event.server.ServerTickStartEvent import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin import dev.jorel.commandapi.CommandAPI import dev.slne.surf.api.core.component.surfComponentApi -import dev.slne.surf.api.core.config.SurfConfigApi import dev.slne.surf.api.paper.event.listen import dev.slne.surf.api.paper.inventory.framework.register import dev.slne.surf.api.paper.nms.NmsUseWithCaution import dev.slne.surf.api.paper.packet.listener.SurfPaperPacketListenerApi import dev.slne.surf.api.paper.test.command.SurfApiTestCommand import dev.slne.surf.api.paper.test.command.subcommands.reflection.Reflection -import dev.slne.surf.api.paper.test.config.TestConfig2 import dev.slne.surf.api.paper.test.listener.ChatListener import dev.slne.surf.surfapi.bukkit.test.command.dialog.dialogTestCommand import dev.slne.surf.surfapi.bukkit.test.command.subcommands.inventory.TestInventoryView @@ -34,7 +32,7 @@ class PaperPluginMain : SuspendingJavaPlugin() { TestInventoryView.register() testInventoryViewDsl.register() - SurfConfigApi.createSpongeYmlConfig(TestConfig2::class.java, dataPath, "test-config-2.yml") +// SurfConfigApi.createSpongeYmlConfig(TestConfig2::class.java, dataPath, "test-config-2.yml") } override suspend fun onEnableAsync() { diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/ModernSerializerTestConfigCommand.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/ModernSerializerTestConfigCommand.kt new file mode 100644 index 000000000..0eb14e801 --- /dev/null +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/command/subcommands/ModernSerializerTestConfigCommand.kt @@ -0,0 +1,17 @@ +package dev.slne.surf.surfapi.bukkit.test.command.subcommands + +import dev.jorel.commandapi.CommandAPICommand +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.subcommand +import dev.slne.surf.surfapi.bukkit.test.config.ModernSerializerTestConfig + +class ModernSerializerTestConfigCommand(name: String): CommandAPICommand(name) { + + init { + subcommand("reload") { + anyExecutor { _, _ -> + ModernSerializerTestConfig.reloadFromFile() + } + } + } +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt new file mode 100644 index 000000000..406708ecc --- /dev/null +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt @@ -0,0 +1,163 @@ +package dev.slne.surf.surfapi.bukkit.test.config + +import dev.slne.surf.api.core.config.SpongeYmlConfigClass +import dev.slne.surf.api.core.config.constraints.* +import dev.slne.surf.api.core.config.serializer.collection.map.ThrowExceptions +import dev.slne.surf.api.core.config.serializer.collection.map.WriteKeyBack +import dev.slne.surf.api.core.config.type.BooleanOrDefault +import dev.slne.surf.api.core.config.type.ConfigDuration +import dev.slne.surf.api.core.config.type.DurationOrDisabled +import dev.slne.surf.api.core.config.type.number.BelowZeroToEmpty +import dev.slne.surf.api.core.config.type.number.DoubleOr +import dev.slne.surf.api.core.config.type.number.IntOr +import dev.slne.surf.surfapi.bukkit.test.plugin +import it.unimi.dsi.fastutil.ints.Int2ObjectMap +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import net.kyori.adventure.key.Key +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextColor +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.io.File +import java.net.URI +import java.net.URL +import java.nio.file.Path +import java.util.* +import java.util.regex.Pattern +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +@ConfigSerializable +data class ModernSerializerTestConfig( + // scalar serializers + val component: Component = Component.text("Hello "), + val key: Key = Key.key("minecraft:stone"), + val duration: ConfigDuration = ConfigDuration(30.seconds), + val uuid: UUID = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), + val regex: Regex = "^[a-z0-9_-]+$".toRegex(), + val pattern: Pattern = Pattern.compile("^[A-Z_]+$"), + val uri: URI = URI("https://example.com/api"), + val url: URL = URI("https://example.com").toURL(), + val path: Path = Path.of("plugins/Surf/test.yml"), + val file: File = File("plugins/Surf/test.yml"), + val textColor: TextColor = TextColor.color(0x55FFFF), + + // wrapper types + val booleanOrDefault: BooleanOrDefault = BooleanOrDefault.USE_DEFAULT, + val durationOrDisabled: DurationOrDisabled = DurationOrDisabled.DISABLED, + val intOrDefault: IntOr.Default = IntOr.Default.USE_DEFAULT, + val intOrDisabled: IntOr.Disabled = IntOr.Disabled.DISABLED, + val doubleOrDefault: DoubleOr.Default = DoubleOr.Default.USE_DEFAULT, + val doubleOrDisabled: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED, + + @BelowZeroToEmpty + val belowZeroIntDefault: IntOr.Default = IntOr.Default.USE_DEFAULT, + + @BelowZeroToEmpty + val belowZeroDoubleDisabled: DoubleOr.Disabled = DoubleOr.Disabled.DISABLED, + + // numeric constraints + @PositiveNumber + val positiveNumber: Int = 1, + + @NegativeNumber + val negativeNumber: Int = -1, + + @MinNumber(10.0) + val minNumber: Int = 10, + + @MaxNumber(100.0) + val maxNumber: Int = 100, + + @Range(min = 0.0, max = 1.0) + val chance: Double = 0.5, + + @NotBlank + val notBlankString: String = "hello", + + @Trimmed + val trimmedString: String = "hello", + + @MinLength(3) + val minLengthString: String = "abc", + + @MaxLength(16) + val maxLengthString: String = "short-text", + + @StartsWith("surf:") + val startsWithString: String = "surf:test", + + @EndsWith(".yml") + val endsWithString: String = "config.yml", + + @Contains("surf") + val containsString: String = "hello-surf-api", + + // collection constraints + @NotEmpty + val notEmptyList: List = listOf("one"), + + @MinSize(1) + val minSizeList: List = listOf("one"), + + @MaxSize(3) + val maxSizeList: List = listOf("one", "two"), + + @NoDuplicates + val noDuplicatesList: List = listOf("one", "two"), + + // duration constraints + @MinDuration(seconds = 5) + val minDuration: ConfigDuration = ConfigDuration(10.seconds), + + @MaxDuration(seconds = 300) + val maxDuration: ConfigDuration = ConfigDuration(5.minutes), + + // key / enum-ish constraints + @Namespace("minecraft") + val namespacedKey: Key = Key.key("minecraft:dirt"), + + @DisallowValues("DISABLED", "NONE") + val mode: TestMode = TestMode.ENABLED, + + // path constraints + @ExistingFile + val existingFile: Path = Path.of("plugins/SurfPaperPluginTest/test.yml"), + + @Directory + val directory: Path = Path.of("plugins/Surf"), + + @WritablePath + val writablePath: Path = Path.of("plugins/SurfPaperPluginTest/test.yml"), + + // map serializers + val normalMap: Map = mapOf( + "one" to 1, + "two" to 2, + ), + + val strictMap: @ThrowExceptions Map = mapOf( + "strict" to 1, + ), + + val keyWriteBackMap: Map<@WriteKeyBack Key, Int> = mapOf( + Key.key("minecraft:stone") to 1, + ), + + // fastutil map serializer + val fastutilMap: Int2ObjectMap = Int2ObjectOpenHashMap().apply { + put(1, "one") + put(2, "two") + }, +) { + companion object : SpongeYmlConfigClass( + ModernSerializerTestConfig::class.java, + plugin.dataPath, + "modern-serializer-test-config.yml" + ) +} + +enum class TestMode { + ENABLED, + DISABLED, + NONE +} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt index 52ec969cf..ab08cd32b 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt @@ -4,6 +4,7 @@ import com.google.auto.service.AutoService import dev.slne.surf.api.core.config.serializer.SpongeConfigSerializers import dev.slne.surf.api.paper.server.PaperInstance import dev.slne.surf.api.paper.server.config.serializers.registry.RegistryValueSerializer +import io.leangen.geantyref.TypeToken import io.papermc.paper.datacomponent.DataComponentType import io.papermc.paper.dialog.Dialog import io.papermc.paper.entity.poi.PoiType @@ -59,7 +60,7 @@ class PaperSpongeConfigSerializers : SpongeConfigSerializers() { builder.register(RegistryValueSerializer(Fluid::class.java, RegistryKey.FLUID, true)) builder.register(RegistryValueSerializer(Sound::class.java, RegistryKey.SOUND_EVENT, true)) builder.register(RegistryValueSerializer(DataComponentType::class.java, RegistryKey.DATA_COMPONENT_TYPE, true)) - builder.register(RegistryValueSerializer(GameRule::class.java, RegistryKey.GAME_RULE, true)) + builder.register(RegistryValueSerializer(object : TypeToken>() {}, RegistryKey.GAME_RULE, true)) builder.register(RegistryValueSerializer(Biome::class.java, RegistryKey.BIOME, true)) builder.register(RegistryValueSerializer(Structure::class.java, RegistryKey.STRUCTURE, true)) builder.register(RegistryValueSerializer(TrimMaterial::class.java, RegistryKey.TRIM_MATERIAL, true)) From eb800249688b0e266ea4c36ff8a5249f7465654d Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 22:49:37 +0200 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=A8=20feat(config):=20add=20new=20v?= =?UTF-8?q?alidation=20annotations=20for=20configuration=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce Contains, Directory, DisallowValues, EndsWith, ExistingFile, MaxDuration, MaxLength, MaxSize, MinDuration, MinSize, Namespace, NegativeNumber, NoDuplicates, NotEmpty, StartsWith, and WritablePath annotations - each annotation provides specific validation logic for configuration fields --- .../api/core/config/constraints/Contains.kt | 8 +++++++ .../api/core/config/constraints/Directory.kt | 21 +++++++++++++++++++ .../core/config/constraints/DisallowValues.kt | 8 +++++++ .../api/core/config/constraints/EndsWith.kt | 8 +++++++ .../core/config/constraints/ExistingFile.kt | 18 ++++++++++++++++ .../core/config/constraints/MaxDuration.kt | 9 ++++++++ .../api/core/config/constraints/MaxLength.kt | 9 ++++++++ .../api/core/config/constraints/MaxSize.kt | 9 ++++++++ .../core/config/constraints/MinDuration.kt | 9 ++++++++ .../api/core/config/constraints/MinSize.kt | 9 ++++++++ .../api/core/config/constraints/Namespace.kt | 9 ++++++++ .../core/config/constraints/NegativeNumber.kt | 6 ++++++ .../core/config/constraints/NoDuplicates.kt | 16 ++++++++++++++ .../api/core/config/constraints/NotEmpty.kt | 7 +++++++ .../api/core/config/constraints/StartsWith.kt | 8 +++++++ .../core/config/constraints/WritablePath.kt | 16 ++++++++++++++ .../api/core/config/type/ConfigDuration.kt | 8 +++++++ 17 files changed, 178 insertions(+) diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt index 5b96f7f4e..46b770659 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Contains.kt @@ -4,6 +4,14 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Requires a string config value to contain a specific substring. + * + * This annotation ensures that the annotated string contains the specified substring. + * If the validation fails, a `SerializationException` is thrown with a descriptive error message. + * + * @property value The substring that must be present in the string value. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class Contains(val value: String) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt index 788ded6ec..183880ded 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Directory.kt @@ -6,6 +6,27 @@ import java.lang.reflect.Type import kotlin.io.path.exists import kotlin.io.path.isDirectory +/** + * Ensures that a configuration value represents a valid, existing directory. + * + * This annotation validates that the annotated value points to a directory that exists on the filesystem. + * If the value does not exist, or if it does not represent a directory, validation will fail with a + * `SerializationException`. + * + * This constraint supports different types of input, such as `Path`, `File`, and `String`, converting + * them to a `Path` internally using the helper method `asPathOrNull`. + * + * Constraints: + * - The value must point to an existing directory. + * - The value must be convertible to a `Path` object. + * + * Intended for use on fields in configuration classes. + * + * Validation failure will throw a `SerializationException` with a descriptive message indicating the + * problem with the directory path. + * + * Associated factory implementation provides the actual validation logic. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class Directory { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt index 9dbbbb433..740e6ad88 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/DisallowValues.kt @@ -4,6 +4,14 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Prevents specific values from being assigned to the annotated field. + * + * This annotation is used to define a set of disallowed string values for a field. If the annotated field's + * value matches any of the specified disallowed values (case-insensitive), a `SerializationException` is thrown. + * + * @property values The array of string values that are disallowed. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class DisallowValues(vararg val values: String) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt index 56ba8c3d1..c013118f1 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/EndsWith.kt @@ -4,6 +4,14 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Validates that a string config value ends with a specified suffix. + * + * This annotation ensures that the annotated string ends with the provided suffix. + * If the validation fails, a `SerializationException` is thrown with a descriptive error message. + * + * @property suffix The suffix that the string value must end with. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class EndsWith(val suffix: String) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt index 7a7c49a19..19601739e 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt @@ -8,6 +8,24 @@ import java.nio.file.Path import kotlin.io.path.exists import kotlin.io.path.isRegularFile +/** + * Requires that the annotated field refers to an existing file. + * + * This annotation ensures that the value of the annotated field corresponds to + * a valid file path that exists and is a regular file. If the validation fails, + * a `SerializationException` is thrown with a descriptive error message. + * + * The value can be: + * - A `Path` object. + * - A `File` object. + * - A `String` representing the file path. + * + * If the value is not convertible to a file path or the file does not exist + * or is not a regular file, the validation fails. + * + * This annotation is typically used for configuration values to ensure + * that specified file paths are valid at runtime. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class ExistingFile { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt index 81a4d422f..1bd481223 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxDuration.kt @@ -5,6 +5,15 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Annotation for constraining a `ConfigDuration`'s value to a maximum duration. + * + * This annotation ensures that the duration value of the annotated field does not exceed + * the specified number of seconds. If the validation fails, a `SerializationException` + * is thrown with a descriptive error message. + * + * @property seconds The maximum duration in seconds that the annotated field can have. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class MaxDuration(val seconds: Long) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt index 92cb302e3..38f979dee 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxLength.kt @@ -4,6 +4,15 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Validates that a string config value does not exceed a specified maximum length. + * + * This annotation is used to ensure that the length of the annotated string + * is less than or equal to the specified maximum value. If the validation fails, + * a `SerializationException` is thrown with a descriptive error message. + * + * @property max The maximum allowed length for the string value. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class MaxLength(val max: Int) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt index b57e326c3..8edf9080c 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxSize.kt @@ -4,6 +4,15 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Specifies a maximum size constraint for collections, maps, arrays, or strings. + * + * This annotation enforces that the size or length of the annotated value does not exceed + * the specified maximum. If the validation fails, a `SerializationException` is thrown + * with a descriptive error message. + * + * @property max The maximum allowed size or length for the annotated value. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class MaxSize(val max: Int) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt index 2aa420ab4..857959695 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinDuration.kt @@ -5,6 +5,15 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Specifies a minimum duration constraint for configuration values annotated with this annotation. + * + * This annotation ensures that durations provided in the configuration meet or exceed + * the specified minimum value in seconds. If the validation fails, a `SerializationException` + * is thrown with a descriptive error message. + * + * @property seconds The minimum allowed duration in seconds. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class MinDuration(val seconds: Long) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt index 106e3f811..823d7b71d 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinSize.kt @@ -4,6 +4,15 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Specifies a minimum size constraint for collections, maps, arrays, or strings. + * + * This annotation enforces that the size or length of the annotated value is at least + * the specified minimum. If the validation fails, a `SerializationException` is thrown + * with a descriptive error message. + * + * @property min The minimum required size or length for the annotated value. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class MinSize(val min: Int) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt index c33865ddd..250b17918 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/Namespace.kt @@ -5,6 +5,15 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Ensures that a key value conforms to a specific namespace. + * + * This annotation validates that the `namespace` of a key matches the expected namespace + * defined in the `Namespace` annotation. If the validation fails, a `SerializationException` + * is thrown with a descriptive error message. + * + * @property namespace The required namespace for the key value. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class Namespace(val namespace: String) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt index 5d783d911..f28f51da0 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NegativeNumber.kt @@ -4,6 +4,12 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Ensures that a numeric config value is strictly negative. + * + * This annotation validates that the annotated numeric value is less than zero. If the + * validation fails, a `SerializationException` is thrown with a descriptive error message. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class NegativeNumber { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt index 28ae969b0..23022e3b5 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NoDuplicates.kt @@ -4,6 +4,22 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Ensures that a collection or array does not contain duplicate elements. + * + * This annotation validates that the annotated value, if it is a collection or array, + * contains only unique elements. If duplicates are found, a `SerializationException` + * is thrown with a descriptive error message. + * + * Supported types: + * - `Iterable`: Validates all elements in the iterable. + * - `Array`: Validates all elements in the array. + * + * Values that are `null` or of unsupported types are ignored during validation. + * + * Exceptions: + * - `SerializationException`: Thrown if the collection or array contains duplicate elements. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class NoDuplicates { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt index 9c4847136..b939aeee4 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/NotEmpty.kt @@ -4,6 +4,13 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Ensures that a collection, map, array, or string config value is not empty. + * + * This annotation enforces that the size or length of the annotated value is greater + * than 0. If the validation fails, a `SerializationException` is thrown with a + * descriptive error message. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class NotEmpty { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt index 37d220c31..255a28ab3 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/StartsWith.kt @@ -4,6 +4,14 @@ import org.spongepowered.configurate.objectmapping.meta.Constraint import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type +/** + * Validates that a string config value starts with a specified prefix. + * + * This annotation ensures that the annotated string begins with the provided prefix. + * If the validation fails, a `SerializationException` is thrown with a descriptive error message. + * + * @property prefix The prefix that the string value must start with. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class StartsWith(val prefix: String) { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt index a7a8c7566..04169e74e 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt @@ -6,6 +6,22 @@ import java.lang.reflect.Type import kotlin.io.path.exists import kotlin.io.path.isWritable +/** + * Validates that a configuration value represents a writable path. + * + * This annotation ensures that the annotated value refers to a file system path + * that exists and is writable. If the validation fails, a `SerializationException` + * will be thrown with a descriptive error message. + * + * Supported value types include: + * - `Path`: Directly represents a file system path. + * - `File`: Converted to a `Path` for validation. + * - `String`: Parsed as a file system path. + * + * Validation checks: + * - If the path exists, it must be writable. + * - If the value cannot be converted to a valid path, it is ignored during validation. + */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) annotation class WritablePath { diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt index 3e90e0f96..233334847 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/type/ConfigDuration.kt @@ -9,6 +9,14 @@ import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.toDuration +/** + * Encapsulates a configuration value representing a duration, with functionality for parsing, + * formatting, and serialization. + * + * @property value The parsed duration value as a [Duration]. + * @property rawValue The original raw input that was used to construct this [ConfigDuration]. + * This is stored for serialization purposes. + */ data class ConfigDuration( val value: Duration, val rawValue: String = format(value) From d3bf30c7980c3801280864dd31638bdeb5c36411 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 22:54:00 +0200 Subject: [PATCH 07/18] =?UTF-8?q?=F0=9F=94=A7=20chore(abi):=20update=20api?= =?UTF-8?q?=20dump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../surf-api-core/api/surf-api-core.api | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/surf-api-core/surf-api-core/api/surf-api-core.api b/surf-api-core/surf-api-core/api/surf-api-core.api index 995c79c50..cc1302c9b 100644 --- a/surf-api-core/surf-api-core/api/surf-api-core.api +++ b/surf-api-core/surf-api-core/api/surf-api-core.api @@ -207,6 +207,60 @@ public final class dev/slne/surf/api/core/config/SurfConfigApiKt { public abstract interface annotation class dev/slne/surf/api/core/config/YamlConfigFileNamePattern : java/lang/annotation/Annotation { } +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Contains : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Contains$Companion; + public abstract fun value ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/Contains$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Directory : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Directory$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/Directory$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/DisallowValues : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/DisallowValues$Companion; + public abstract fun values ()[Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/DisallowValues$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/EndsWith : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/EndsWith$Companion; + public abstract fun suffix ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/EndsWith$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/ExistingFile : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/ExistingFile$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/ExistingFile$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxDuration : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxDuration$Companion; + public abstract fun seconds ()J +} + +public final class dev/slne/surf/api/core/config/constraints/MaxDuration$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxLength : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxLength$Companion; + public abstract fun max ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MaxLength$Companion { +} + public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxNumber : java/lang/annotation/Annotation { public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxNumber$Companion; public abstract fun max ()D @@ -215,6 +269,30 @@ public abstract interface annotation class dev/slne/surf/api/core/config/constra public final class dev/slne/surf/api/core/config/constraints/MaxNumber$Companion { } +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MaxSize : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MaxSize$Companion; + public abstract fun max ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MaxSize$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinDuration : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinDuration$Companion; + public abstract fun seconds ()J +} + +public final class dev/slne/surf/api/core/config/constraints/MinDuration$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinLength : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinLength$Companion; + public abstract fun min ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MinLength$Companion { +} + public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinNumber : java/lang/annotation/Annotation { public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinNumber$Companion; public abstract fun min ()D @@ -223,6 +301,50 @@ public abstract interface annotation class dev/slne/surf/api/core/config/constra public final class dev/slne/surf/api/core/config/constraints/MinNumber$Companion { } +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/MinSize : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/MinSize$Companion; + public abstract fun min ()I +} + +public final class dev/slne/surf/api/core/config/constraints/MinSize$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Namespace : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Namespace$Companion; + public abstract fun namespace ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/Namespace$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NegativeNumber : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NegativeNumber$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NegativeNumber$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NoDuplicates : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NoDuplicates$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NoDuplicates$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NotBlank : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NotBlank$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NotBlank$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/NotEmpty : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/NotEmpty$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/NotEmpty$Companion { +} + public abstract interface annotation class dev/slne/surf/api/core/config/constraints/PositiveNumber : java/lang/annotation/Annotation { public static final field Companion Ldev/slne/surf/api/core/config/constraints/PositiveNumber$Companion; } @@ -230,6 +352,37 @@ public abstract interface annotation class dev/slne/surf/api/core/config/constra public final class dev/slne/surf/api/core/config/constraints/PositiveNumber$Companion { } +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Range : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Range$Companion; + public abstract fun max ()D + public abstract fun min ()D +} + +public final class dev/slne/surf/api/core/config/constraints/Range$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/StartsWith : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/StartsWith$Companion; + public abstract fun prefix ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/constraints/StartsWith$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/Trimmed : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/Trimmed$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/Trimmed$Companion { +} + +public abstract interface annotation class dev/slne/surf/api/core/config/constraints/WritablePath : java/lang/annotation/Annotation { + public static final field Companion Ldev/slne/surf/api/core/config/constraints/WritablePath$Companion; +} + +public final class dev/slne/surf/api/core/config/constraints/WritablePath$Companion { +} + public final class dev/slne/surf/api/core/config/manager/LoadConfigException : java/lang/RuntimeException { public static final field Companion Ldev/slne/surf/api/core/config/manager/LoadConfigException$Companion; public fun (Ljava/lang/String;)V @@ -310,6 +463,7 @@ public abstract class dev/slne/surf/api/core/config/serializer/SpongeConfigSeria public final fun getClassSerializers ()Lit/unimi/dsi/fastutil/objects/Object2ObjectMap; public final fun getTypeTokenSerializers ()Lit/unimi/dsi/fastutil/objects/Object2ObjectMap; public final fun registerClassSerializer (Ljava/lang/Class;Lorg/spongepowered/configurate/serialize/TypeSerializer;)V + protected fun registerDefaults (Lorg/spongepowered/configurate/serialize/TypeSerializerCollection$Builder;)V public final fun registerTypeTokenSerializer (Lio/leangen/geantyref/TypeToken;Lorg/spongepowered/configurate/serialize/TypeSerializer;)V public final fun unregisterSerializer (Ljava/lang/Class;)V public final fun unregisterTypeTokenSerializer (Lio/leangen/geantyref/TypeToken;)V @@ -382,6 +536,28 @@ public final class dev/slne/surf/api/core/config/type/BooleanOrDefault { public final class dev/slne/surf/api/core/config/type/BooleanOrDefault$Companion { } +public final class dev/slne/surf/api/core/config/type/ConfigDuration { + public static final field Companion Ldev/slne/surf/api/core/config/type/ConfigDuration$Companion; + public synthetic fun (JLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun asDuration-UwyO8pc ()J + public final fun component1-UwyO8pc ()J + public final fun component2 ()Ljava/lang/String; + public final fun copy-VtjQ1oo (JLjava/lang/String;)Ldev/slne/surf/api/core/config/type/ConfigDuration; + public static synthetic fun copy-VtjQ1oo$default (Ldev/slne/surf/api/core/config/type/ConfigDuration;JLjava/lang/String;ILjava/lang/Object;)Ldev/slne/surf/api/core/config/type/ConfigDuration; + public fun equals (Ljava/lang/Object;)Z + public final fun getRawValue ()Ljava/lang/String; + public final fun getValue-UwyO8pc ()J + public fun hashCode ()I + public final fun inWholeSeconds ()J + public fun toString ()Ljava/lang/String; +} + +public final class dev/slne/surf/api/core/config/type/ConfigDuration$Companion { + public final fun format-LRDsOJo (J)Ljava/lang/String; + public final fun parse (Ljava/lang/String;)Ldev/slne/surf/api/core/config/type/ConfigDuration; +} + public final class dev/slne/surf/api/core/config/type/DurationOrDisabled { public static final field Companion Ldev/slne/surf/api/core/config/type/DurationOrDisabled$Companion; public static final field DISABLED Ldev/slne/surf/api/core/config/type/DurationOrDisabled; From 75a8ce0711895c1d872944873d09c0181a431110 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 22:54:14 +0200 Subject: [PATCH 08/18] =?UTF-8?q?=F0=9F=94=A7=20chore:=20update=20version?= =?UTF-8?q?=20to=203.13.0=20in=20gradle.properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f1e646c9f..f96891f30 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.1.2 group=dev.slne.surf.api -version=3.12.0 +version=3.13.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false From c6fcd2dba9b4c1b22ddfc8a2ed05d5735a5bc37a Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 22:54:51 +0200 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=94=A7=20chore:=20downgrade=20versi?= =?UTF-8?q?on=20from=203.13.0=20to=203.12.0=20in=20gradle.properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f96891f30..f1e646c9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.1.2 group=dev.slne.surf.api -version=3.13.0 +version=3.12.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false From 495bbd9d85d72c7108d59a1386c591275d8a9f7f Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 23:18:41 +0200 Subject: [PATCH 10/18] =?UTF-8?q?=E2=9C=A8=20feat(config):=20add=20new=20c?= =?UTF-8?q?onfiguration=20types=20and=20update=20serializers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - introduce ItemStack, Location, Material, and GameRule types in ModernSerializerTestConfig - register new serializers for ItemStack and Location in PaperSpongeConfigSerializers - change registryKey visibility to protected in RegistryEntrySerializer - improve error message in RegistryValueSerializer for missing values --- .../test/config/ModernSerializerTestConfig.kt | 15 +++++++ .../config/serializers/LocationSerializer.kt | 42 ------------------- .../PaperSpongeConfigSerializers.kt | 3 -- .../config/serializers/VectorSerializer.kt | 27 ------------ .../registry/RegistryEntrySerializer.kt | 2 +- .../registry/RegistryValueSerializer.kt | 2 +- 6 files changed, 17 insertions(+), 74 deletions(-) delete mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt delete mode 100644 surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt index 406708ecc..8dd57cf62 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt @@ -16,6 +16,12 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import net.kyori.adventure.key.Key import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.TextColor +import org.bukkit.GameRule +import org.bukkit.GameRules +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.ItemType import org.spongepowered.configurate.objectmapping.ConfigSerializable import java.io.File import java.net.URI @@ -148,6 +154,15 @@ data class ModernSerializerTestConfig( put(1, "one") put(2, "two") }, + + // paper + val stack: ItemStack = ItemType.BOOK.createItemStack(), + + val location: Location = Location(plugin.server.worlds.first(), 0.0, 0.0, 0.0), + + val itemType: ItemType = ItemType.STONE, + val material: Material = Material.STONE, + val selectedGameRules: List> = listOf(GameRules.ADVANCE_TIME, GameRules.MAX_BLOCK_MODIFICATIONS), ) { companion object : SpongeYmlConfigClass( ModernSerializerTestConfig::class.java, diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt deleted file mode 100644 index 4644a9364..000000000 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/LocationSerializer.kt +++ /dev/null @@ -1,42 +0,0 @@ -package dev.slne.surf.api.paper.server.config.serializers - -import net.kyori.adventure.key.Key -import org.bukkit.Bukkit -import org.bukkit.Location -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.serialize.SerializationException -import org.spongepowered.configurate.serialize.TypeSerializer -import java.lang.reflect.Type - -internal object LocationSerializer : TypeSerializer { - override fun deserialize(type: Type, node: ConfigurationNode): Location { - val worldKey = node.node("world-key").get(Key::class.java) - ?: throw SerializationException(Location::class.java, "Location is missing world") - - val world = Bukkit.getWorld(worldKey) - ?: throw SerializationException(Location::class.java, "Unknown world: $worldKey") - - return Location( - world, - node.node("x").double, - node.node("y").double, - node.node("z").double, - node.node("yaw").float, - node.node("pitch").float - ) - } - - override fun serialize(type: Type, obj: Location?, node: ConfigurationNode) { - if (obj == null) { - node.raw(null) - return - } - - node.node("world").set(obj.world.key) - node.node("x").set(obj.x) - node.node("y").set(obj.y) - node.node("z").set(obj.z) - node.node("yaw").set(obj.yaw) - node.node("pitch").set(obj.pitch) - } -} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt index ab08cd32b..b39d07e25 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/PaperSpongeConfigSerializers.kt @@ -37,9 +37,6 @@ class PaperSpongeConfigSerializers : SpongeConfigSerializers() { builder.register(NamespacedKeySerializer) builder.register(MaterialSerializer) -// builder.register(Vector::class.java, VectorSerializer) - builder.register(Location::class.java, LocationSerializer) - if (!PaperInstance.isBootstrapping()) { builder.register(ItemStackSerializer) diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt deleted file mode 100644 index b1bd3780b..000000000 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/VectorSerializer.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.slne.surf.api.paper.server.config.serializers - -import org.bukkit.util.Vector -import org.spongepowered.configurate.ConfigurationNode -import org.spongepowered.configurate.serialize.TypeSerializer -import java.lang.reflect.Type - -internal object VectorSerializer : TypeSerializer { - override fun deserialize(type: Type, node: ConfigurationNode): Vector { - return Vector( - node.node("x").double, - node.node("y").double, - node.node("z").double - ) - } - - override fun serialize(type: Type, obj: Vector?, node: ConfigurationNode) { - if (obj == null) { - node.raw(null) - return - } - - node.node("x").set(obj.x) - node.node("y").set(obj.y) - node.node("z").set(obj.z) - } -} \ No newline at end of file diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt index a573a24ae..b3de83bca 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt @@ -14,7 +14,7 @@ import java.util.function.Predicate abstract class RegistryEntrySerializer : ScalarSerializer.Annotated { - private val registryKey: RegistryKey + protected val registryKey: RegistryKey private val omitMinecraftNamespace: Boolean protected constructor( diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt index a117febb2..bd0cbb424 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryValueSerializer.kt @@ -21,7 +21,7 @@ class RegistryValueSerializer : RegistryEntrySerializer { override fun convertFromResourceKey(key: Key): T { val value = registry().get(key) - ?: throw SerializationException("Missing value in ${registry()} with string key: ${key.asString()}") + ?: throw SerializationException("Missing value in $registryKey with string key: ${key.asString()}") return value } From 2e45982180897fea6dfdcd0980a6eacec39e8ee3 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 23:20:00 +0200 Subject: [PATCH 11/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(serializers?= =?UTF-8?q?):=20simplify=20key=20serialization=20logic=20in=20RegistryEntr?= =?UTF-8?q?ySerializer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/serializers/registry/RegistryEntrySerializer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt index b3de83bca..3130bafb6 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/registry/RegistryEntrySerializer.kt @@ -49,8 +49,8 @@ abstract class RegistryEntrySerializer : ScalarSerializer.Annotate override fun serialize(type: AnnotatedType, item: T, typeSupported: Predicate>): Any { val key = this.convertToResourceKey(item) - return if (this.omitMinecraftNamespace && key.key().namespace() == Key.MINECRAFT_NAMESPACE) { - key.key().value() + return if (this.omitMinecraftNamespace && key.namespace() == Key.MINECRAFT_NAMESPACE) { + key.value() } else { key.asString() } From b05371c8adc352dd416dad66b95b92fa77f66d40 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Sun, 17 May 2026 23:24:07 +0200 Subject: [PATCH 12/18] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../slne/surf/api/core/config/constraints/WritablePath.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt index 04169e74e..a41fb48b6 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/WritablePath.kt @@ -7,11 +7,11 @@ import kotlin.io.path.exists import kotlin.io.path.isWritable /** - * Validates that a configuration value represents a writable path. + * Validates that a configuration value represents a writable path when the path exists. * * This annotation ensures that the annotated value refers to a file system path - * that exists and is writable. If the validation fails, a `SerializationException` - * will be thrown with a descriptive error message. + * that is writable if it already exists. If the validation fails for an existing path, + * a `SerializationException` will be thrown with a descriptive error message. * * Supported value types include: * - `Path`: Directly represents a file system path. From c50eafb0a9c8cf730715d58eac30ab35b3f91749 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 21:36:55 +0000 Subject: [PATCH 13/18] fix: address review feedback - SerializationException, AnnotatedType, FastutilMapSerializer, ItemStackSerializer Agent-Logs-Url: https://github.com/SLNE-Development/surf-api/sessions/17b51bf8-8b45-42c0-8bff-7ef2c688ffa1 Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../api/core/config/constraints/MaxNumber.kt | 3 +- .../api/core/config/constraints/MinNumber.kt | 3 +- .../config/serializer/EnumValueSerializer.kt | 12 ++- .../collection/map/FastutilMapSerializer.kt | 73 +++++++++++++++---- .../collection/map/MapSerializer.kt | 15 ++-- .../config/serializers/ItemStackSerializer.kt | 12 ++- 6 files changed, 85 insertions(+), 33 deletions(-) diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt index 1d61a2642..cbe07763f 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MaxNumber.kt @@ -1,6 +1,7 @@ package dev.slne.surf.api.core.config.constraints import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type /** @@ -30,7 +31,7 @@ annotation class MaxNumber(val max: Double) { internal object Factory : Constraint.Factory { override fun make(data: MaxNumber, type: Type): Constraint = { number -> if (number != null && number.toDouble() > data.max) { - throw IllegalArgumentException("Number is too big: $number, expected <= ${data.max}") + throw SerializationException(type, "Number is too big: $number, expected <= ${data.max}") } } } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt index bccc8c4a8..1d6edca4b 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/MinNumber.kt @@ -1,6 +1,7 @@ package dev.slne.surf.api.core.config.constraints import org.spongepowered.configurate.objectmapping.meta.Constraint +import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.Type /** @@ -30,7 +31,7 @@ annotation class MinNumber(val min: Double) { internal object Factory : Constraint.Factory { override fun make(data: MinNumber, type: Type): Constraint = { number -> if (number != null && number.toDouble() < data.min) { - throw IllegalArgumentException("Number is too small: $number, expected >= ${data.min}") + throw SerializationException(type, "Number is too small: $number, expected >= ${data.min}") } } } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt index 1b552b65c..392d24c0b 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/EnumValueSerializer.kt @@ -2,8 +2,8 @@ package dev.slne.surf.api.core.config.serializer import io.leangen.geantyref.GenericTypeReflector import io.leangen.geantyref.TypeToken -import net.kyori.adventure.text.logger.slf4j.ComponentLogger import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException import org.spongepowered.configurate.util.EnumLookup import java.lang.reflect.AnnotatedType import java.util.function.Predicate @@ -15,12 +15,11 @@ import java.util.function.Predicate * underscores are converted to dashes and lookup is attempted again. */ internal object EnumValueSerializer : ScalarSerializer.Annotated>(object : TypeToken>() {}) { - private val LOGGER = ComponentLogger.logger() /** * Resolves a serialized value to an enum constant of the requested enum type. */ - override fun deserialize(type: AnnotatedType, obj: Any): Enum<*>? { + override fun deserialize(type: AnnotatedType, obj: Any): Enum<*> { val constant = obj.toString() val typeClass = GenericTypeReflector.erase(type.type).asSubclass(Enum::class.java) @@ -30,10 +29,9 @@ internal object EnumValueSerializer : ScalarSerializer.Annotated>(object } if (foundEnum == null) { val joinedEnumOptions = typeClass.enumConstants.joinToString(limit = 10) - LOGGER.error( - "Invalid enum constant provided, expected one of [{}], but got {}", - joinedEnumOptions, - constant + throw SerializationException( + type.type, + "Invalid enum constant '$constant' for ${typeClass.simpleName}, expected one of: [$joinedEnumOptions]" ) } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt index 4a7ec5527..a97cdda5a 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer.kt @@ -40,7 +40,7 @@ abstract class FastutilMapSerializer>( * fastutil map implementation. */ override fun deserialize(type: AnnotatedType, node: ConfigurationNode): M { - val mapType = createAnnotatedMapType(type as AnnotatedParameterizedType) + val mapType = createAnnotatedMapType(type) val map = node.get(mapType) as? Map ?: emptyMap() return factory(map) @@ -57,19 +57,27 @@ abstract class FastutilMapSerializer>( return } - node.set(createAnnotatedMapType(type as AnnotatedParameterizedType), obj) + node.set(createAnnotatedMapType(type), obj) } - private fun createAnnotatedMapType(type: AnnotatedParameterizedType): AnnotatedType { - val baseType = createBaseMapType(type.type as ParameterizedType) - return GenericTypeReflector.annotate(baseType, type.annotations) + private fun createAnnotatedMapType(type: AnnotatedType): AnnotatedType { + val paramType = type as? AnnotatedParameterizedType + val baseType = createBaseMapType(paramType) + return if (paramType != null) { + GenericTypeReflector.annotate(baseType, paramType.annotations) + } else { + GenericTypeReflector.annotate(baseType) + } } /** * Creates the regular boxed [Map] type that Configurate should use internally. + * + * @param type The parameterized type, or `null` for non-generic fastutil maps + * (e.g. `Int2IntMap`) where both types are known primitives. */ - protected abstract fun createBaseMapType(type: ParameterizedType): Type + protected abstract fun createBaseMapType(type: AnnotatedParameterizedType?): Type /** * Serializer variant for maps with a regular object-like key and a primitive value. @@ -86,10 +94,13 @@ abstract class FastutilMapSerializer>( /** * Creates a `Map` type. */ - override fun createBaseMapType(type: ParameterizedType): Type { + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { + requireNotNull(type) { + "SomethingToPrimitive requires a parameterized type with a key type argument" + } return TypeFactory.parameterizedClass( Map::class.java, - type.actualTypeArguments[0], + (type.type as ParameterizedType).actualTypeArguments[0], GenericTypeReflector.box(primitiveType) ) } @@ -110,11 +121,14 @@ abstract class FastutilMapSerializer>( /** * Creates a `Map` type. */ - override fun createBaseMapType(type: ParameterizedType): Type { + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { + requireNotNull(type) { + "PrimitiveToSomething requires a parameterized type with a value type argument" + } return TypeFactory.parameterizedClass( Map::class.java, GenericTypeReflector.box(primitiveType), - type.actualTypeArguments[0] + (type.type as ParameterizedType).actualTypeArguments[0] ) } } @@ -133,11 +147,44 @@ abstract class FastutilMapSerializer>( /** * Creates a `Map` type. */ - override fun createBaseMapType(type: ParameterizedType): Type { + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { + requireNotNull(type) { + "SomethingToSomething requires a parameterized type with key and value type arguments" + } + val typeArgs = (type.type as ParameterizedType).actualTypeArguments + return TypeFactory.parameterizedClass( + Map::class.java, + typeArgs[0], + typeArgs[1] + ) + } + } + + /** + * Serializer variant for maps where both key and value are fixed primitive types. + * + * Use this for non-generic fastutil maps such as `Int2IntMap`, `Long2LongMap`, + * `Int2LongMap`, etc., which have no type parameters. + * + * Example fastutil types: + * - `Int2IntMap` + * - `Long2LongMap` + * - `Int2LongMap` + */ + class PrimitiveToPrimitive>( + factory: (Map) -> M, + private val keyPrimitiveType: Type, + private val valuePrimitiveType: Type + ) : FastutilMapSerializer(factory) { + + /** + * Creates a `Map` type. + */ + override fun createBaseMapType(type: AnnotatedParameterizedType?): Type { return TypeFactory.parameterizedClass( Map::class.java, - type.actualTypeArguments[0], - type.actualTypeArguments[1] + GenericTypeReflector.box(keyPrimitiveType), + GenericTypeReflector.box(valuePrimitiveType) ) } } diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt index f7de468e9..be94d34e7 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/collection/map/MapSerializer.kt @@ -10,7 +10,6 @@ import org.spongepowered.configurate.serialize.TypeSerializer import org.spongepowered.configurate.serialize.TypeSerializerCollection import java.lang.reflect.AnnotatedParameterizedType import java.lang.reflect.AnnotatedType -import java.lang.reflect.Type /** * Fault-tolerant Configurate serializer for maps. @@ -81,7 +80,7 @@ internal class MapSerializer( for ((rawKey, valueNode) in node.childrenMap()) { val deserializedKey = deserializePart( - keyType.type, + keyType, keySerializer, "key", keyNode.set(rawKey), @@ -89,7 +88,7 @@ internal class MapSerializer( ) val deserializedValue = deserializePart( - valueType.type, + valueType, valueSerializer, "value", valueNode, @@ -102,7 +101,7 @@ internal class MapSerializer( if (writeKeyBack) { val shouldKeep = serializePart( - keyType.type, + keyType, keySerializer, deserializedKey, "key", @@ -177,14 +176,14 @@ internal class MapSerializer( val keyNode = BasicConfigurationNode.root(node.options()) for ((key, value) in obj) { - if (!serializePart(keyType.type, keySerializer, key, "key", keyNode, node.path())) { + if (!serializePart(keyType, keySerializer, key, "key", keyNode, node.path())) { continue } val keyObj = requireNotNull(keyNode.raw()) { "Key must not be null!" } val child = node.node(keyObj) - serializePart(valueType.type, valueSerializer, value, "value", child, child.path()) + serializePart(valueType, valueSerializer, value, "value", child, child.path()) unvisitedKeys -= keyObj } @@ -208,7 +207,7 @@ internal class MapSerializer( } private fun deserializePart( - type: Type, + type: AnnotatedType, serializer: TypeSerializer<*>, mapPart: String, node: ConfigurationNode, @@ -232,7 +231,7 @@ internal class MapSerializer( @Suppress("UNCHECKED_CAST") private fun serializePart( - type: Type, + type: AnnotatedType, serializer: TypeSerializer<*>, obj: Any?, mapPart: String, diff --git a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt index cf3b2905c..478cfa9d6 100644 --- a/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt +++ b/surf-api-paper/surf-api-paper-server/src/main/kotlin/dev/slne/surf/api/paper/server/config/serializers/ItemStackSerializer.kt @@ -2,6 +2,7 @@ package dev.slne.surf.api.paper.server.config.serializers import org.bukkit.inventory.ItemStack import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.AnnotatedType import java.util.function.Predicate import kotlin.io.encoding.Base64 @@ -10,9 +11,14 @@ object ItemStackSerializer : ScalarSerializer.Annotated(ItemStack::cl override fun deserialize(type: AnnotatedType, obj: Any): ItemStack { val base64 = obj.toString() - val bytes = Base64.decode(base64) - - return ItemStack.deserializeBytes(bytes) + return try { + val bytes = Base64.decode(base64) + ItemStack.deserializeBytes(bytes) + } catch (e: IllegalArgumentException) { + throw SerializationException(type.type, "Invalid Base64-encoded ItemStack: ${e.message}", e) + } catch (e: Exception) { + throw SerializationException(type.type, "Failed to deserialize ItemStack: ${e.message}", e) + } } override fun serialize(type: AnnotatedType, item: ItemStack?, typeSupported: Predicate>): Any? { From 52f43db4f829704232500be707072111f1f03d39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 21:58:33 +0000 Subject: [PATCH 14/18] fix: use PrimitiveToPrimitive for all non-parameterized fastutil primitive maps Agent-Logs-Url: https://github.com/SLNE-Development/surf-api/sessions/247a5a2a-4a98-4d2f-a17e-866fc3012143 Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com> --- .../serializer/SpongeConfigSerializers.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt index a4b6237a2..e36053d5f 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/SpongeConfigSerializers.kt @@ -112,26 +112,26 @@ abstract class SpongeConfigSerializers { builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2LongOpenHashMap(it as Map<*, Long>) }, java.lang.Long.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Reference2ShortOpenHashMap(it as Map<*, Short>) }, java.lang.Short.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToSomething>({ Reference2ObjectOpenHashMap(it) })) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2BooleanOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2ByteOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2CharOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2DoubleOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2FloatOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.SomethingToSomething({ Int2IntOpenHashMap(it as Map) })) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2LongOpenHashMap(it as Map) }, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2BooleanOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2ByteOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Byte.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2CharOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Character.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2DoubleOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Double.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2FloatOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Float.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2IntOpenHashMap(it as Map) }, Integer.TYPE, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2LongOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Long.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ObjectOpenHashMap(it as Map) }, Integer.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Int2ReferenceOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Int2ShortOpenHashMap(it as Map) }, Integer.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2BooleanOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2ByteOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2CharOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2DoubleOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2FloatOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2IntOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.SomethingToSomething({ Long2LongOpenHashMap(it as Map) })) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Int2ShortOpenHashMap(it as Map) }, Integer.TYPE, java.lang.Short.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2BooleanOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Boolean.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2ByteOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Byte.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2CharOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Character.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2DoubleOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Double.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2FloatOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Float.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2IntOpenHashMap(it as Map) }, java.lang.Long.TYPE, Integer.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2LongOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Long.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ObjectOpenHashMap(it as Map) }, java.lang.Long.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.PrimitiveToSomething>({ Long2ReferenceOpenHashMap(it as Map) }, java.lang.Long.TYPE)) - builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToSomething({ Long2ShortOpenHashMap(it as Map) }, java.lang.Long.TYPE)) + builder.register(object : TypeToken() {}, FastutilMapSerializer.PrimitiveToPrimitive({ Long2ShortOpenHashMap(it as Map) }, java.lang.Long.TYPE, java.lang.Short.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2BooleanOpenHashMap(it as Map<*, Boolean>) }, java.lang.Boolean.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2ByteOpenHashMap(it as Map<*, Byte>) }, java.lang.Byte.TYPE)) builder.register(object : TypeToken>() {}, FastutilMapSerializer.SomethingToPrimitive>({ Object2CharOpenHashMap(it as Map<*, Char>) }, java.lang.Character.TYPE)) From b52a1d9f4e5c5397caff1a0e1a469fe6252fa7b6 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Mon, 18 May 2026 00:10:52 +0200 Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=90=9B=20fix(ExistingFile):=20handl?= =?UTF-8?q?e=20potential=20exception=20in=20string=20to=20Path=20conversio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use runCatching to safely convert string to Path, returning null on failure --- .../dev/slne/surf/api/core/config/constraints/ExistingFile.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt index 19601739e..047598548 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/constraints/ExistingFile.kt @@ -46,7 +46,7 @@ internal fun Any?.asPathOrNull(): Path? { null -> null is Path -> this is File -> toPath() - is String -> Path.of(this) + is String -> runCatching { Path.of(this) }.getOrNull() else -> null } } \ No newline at end of file From 41f99b876d24d7806433184ece2aa0f8d3aa1f35 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Mon, 18 May 2026 00:11:07 +0200 Subject: [PATCH 16/18] =?UTF-8?q?=E2=9C=A8=20feat(serializers):=20add=20fa?= =?UTF-8?q?stutil=20Int2IntMap=20for=20integer=20key-value=20pairs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bukkit/test/config/ModernSerializerTestConfig.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt index 8dd57cf62..fe511dd72 100644 --- a/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt +++ b/surf-api-paper/surf-api-paper-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/config/ModernSerializerTestConfig.kt @@ -11,6 +11,8 @@ import dev.slne.surf.api.core.config.type.number.BelowZeroToEmpty import dev.slne.surf.api.core.config.type.number.DoubleOr import dev.slne.surf.api.core.config.type.number.IntOr import dev.slne.surf.surfapi.bukkit.test.plugin +import it.unimi.dsi.fastutil.ints.Int2IntMap +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import net.kyori.adventure.key.Key @@ -155,6 +157,11 @@ data class ModernSerializerTestConfig( put(2, "two") }, + val fastUtilInt2Int: Int2IntMap = Int2IntOpenHashMap().apply { + put(1, 1) + put(2, 2) + }, + // paper val stack: ItemStack = ItemType.BOOK.createItemStack(), From 48c45d778a4600b4353b24bee54d659006876de2 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Mon, 18 May 2026 00:11:20 +0200 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=94=A7=20chore(abi):=20update=20api?= =?UTF-8?q?=20dump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- surf-api-core/surf-api-core/api/surf-api-core.api | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/surf-api-core/surf-api-core/api/surf-api-core.api b/surf-api-core/surf-api-core/api/surf-api-core.api index cc1302c9b..ded822807 100644 --- a/surf-api-core/surf-api-core/api/surf-api-core.api +++ b/surf-api-core/surf-api-core/api/surf-api-core.api @@ -494,13 +494,17 @@ public final class dev/slne/surf/api/core/config/serializer/SpongeConfigSerializ public abstract class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer : org/spongepowered/configurate/serialize/TypeSerializer$Annotated { public fun (Lkotlin/jvm/functions/Function1;)V - protected abstract fun createBaseMapType (Ljava/lang/reflect/ParameterizedType;)Ljava/lang/reflect/Type; + protected abstract fun createBaseMapType (Ljava/lang/reflect/AnnotatedParameterizedType;)Ljava/lang/reflect/Type; public synthetic fun deserialize (Ljava/lang/reflect/AnnotatedType;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/lang/Object; public fun deserialize (Ljava/lang/reflect/AnnotatedType;Lorg/spongepowered/configurate/ConfigurationNode;)Ljava/util/Map; public synthetic fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/lang/Object;Lorg/spongepowered/configurate/ConfigurationNode;)V public fun serialize (Ljava/lang/reflect/AnnotatedType;Ljava/util/Map;Lorg/spongepowered/configurate/ConfigurationNode;)V } +public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$PrimitiveToPrimitive : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/reflect/Type;Ljava/lang/reflect/Type;)V +} + public final class dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer$PrimitiveToSomething : dev/slne/surf/api/core/config/serializer/collection/map/FastutilMapSerializer { public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/reflect/Type;)V } From 5de7d7538cf3a06ba182ff3e167f2a98f34c8fe4 Mon Sep 17 00:00:00 2001 From: twisti <76837088+twisti-dev@users.noreply.github.com> Date: Mon, 18 May 2026 00:13:08 +0200 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=90=9B=20fix(PathSerializer):=20han?= =?UTF-8?q?dle=20InvalidPathException=20during=20path=20deserialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add try-catch block to catch InvalidPathException - throw SerializationException with detailed message on failure --- .../surf/api/core/config/serializer/PathSerializer.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt index f16a4d4d2..fae49a4e1 100644 --- a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/config/serializer/PathSerializer.kt @@ -1,13 +1,19 @@ package dev.slne.surf.api.core.config.serializer import org.spongepowered.configurate.serialize.ScalarSerializer +import org.spongepowered.configurate.serialize.SerializationException import java.lang.reflect.AnnotatedType +import java.nio.file.InvalidPathException import java.nio.file.Path import java.util.function.Predicate internal object PathSerializer : ScalarSerializer.Annotated(Path::class.java) { override fun deserialize(type: AnnotatedType, obj: Any): Path { - return Path.of(obj.toString()) + try { + return Path.of(obj.toString()) + } catch (e: InvalidPathException) { + throw SerializationException(Path::class.java, "$obj($type) is not a valid path", e) + } } override fun serialize(type: AnnotatedType, item: Path, typeSupported: Predicate>): Any {