Skip to content

Commit de6ff77

Browse files
authored
Add new configuration types and serializers for numeric and boolean values (#353)
2 parents 23955b6 + 5de7d75 commit de6ff77

61 files changed

Lines changed: 3208 additions & 65 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
77
javaVersion=25
88
mcVersion=26.1.2
99
group=dev.slne.surf.api
10-
version=3.11.2
10+
version=3.12.0
1111
relocationPrefix=dev.slne.surf.api.libs
1212
snapshot=false

surf-api-core/surf-api-core-server/src/main/kotlin/dev/slne/surf/api/core/server/CoreInstance.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import dev.slne.surf.api.core.messages.Colors
44
import dev.slne.surf.api.core.server.listener.CoreListenerManager
55
import dev.slne.surf.api.core.server.util.PlayerSkinFetcher
66
import org.jetbrains.annotations.MustBeInvokedByOverriders
7+
import java.util.concurrent.atomic.AtomicBoolean
78

89
abstract class CoreInstance {
10+
private val bootstrapping = AtomicBoolean(true)
911

1012
@MustBeInvokedByOverriders
1113
open suspend fun bootstrap() {
@@ -14,6 +16,7 @@ abstract class CoreInstance {
1416

1517
@MustBeInvokedByOverriders
1618
open suspend fun onLoad() {
19+
bootstrapping.set(false)
1720
}
1821

1922
@MustBeInvokedByOverriders
@@ -30,4 +33,6 @@ abstract class CoreInstance {
3033
PlayerSkinFetcher
3134
Colors
3235
}
36+
37+
fun isBootstrapping(): Boolean = bootstrapping.get()
3338
}

surf-api-core/surf-api-core/api/surf-api-core.api

Lines changed: 377 additions & 5 deletions
Large diffs are not rendered by default.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.slne.surf.api.core.config.constraints
2+
3+
internal fun Any?.configSizeOrNull(): Int? = when (this) {
4+
null -> null
5+
is Collection<*> -> size
6+
is Map<*, *> -> size
7+
is Array<*> -> size
8+
is CharSequence -> length
9+
else -> null
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.slne.surf.api.core.config.constraints
2+
3+
import org.spongepowered.configurate.objectmapping.meta.Constraint
4+
import org.spongepowered.configurate.serialize.SerializationException
5+
import java.lang.reflect.Type
6+
7+
/**
8+
* Requires a string config value to contain a specific substring.
9+
*
10+
* This annotation ensures that the annotated string contains the specified substring.
11+
* If the validation fails, a `SerializationException` is thrown with a descriptive error message.
12+
*
13+
* @property value The substring that must be present in the string value.
14+
*/
15+
@Target(AnnotationTarget.FIELD)
16+
@Retention(AnnotationRetention.RUNTIME)
17+
annotation class Contains(val value: String) {
18+
companion object {
19+
internal object Factory : Constraint.Factory<Contains, String?> {
20+
override fun make(data: Contains, type: Type): Constraint<String?> = { value ->
21+
if (value != null && !value.contains(data.value)) {
22+
throw SerializationException("String must contain '${data.value}'")
23+
}
24+
}
25+
}
26+
}
27+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package dev.slne.surf.api.core.config.constraints
2+
3+
import org.spongepowered.configurate.objectmapping.meta.Constraint
4+
import org.spongepowered.configurate.serialize.SerializationException
5+
import java.lang.reflect.Type
6+
import kotlin.io.path.exists
7+
import kotlin.io.path.isDirectory
8+
9+
/**
10+
* Ensures that a configuration value represents a valid, existing directory.
11+
*
12+
* This annotation validates that the annotated value points to a directory that exists on the filesystem.
13+
* If the value does not exist, or if it does not represent a directory, validation will fail with a
14+
* `SerializationException`.
15+
*
16+
* This constraint supports different types of input, such as `Path`, `File`, and `String`, converting
17+
* them to a `Path` internally using the helper method `asPathOrNull`.
18+
*
19+
* Constraints:
20+
* - The value must point to an existing directory.
21+
* - The value must be convertible to a `Path` object.
22+
*
23+
* Intended for use on fields in configuration classes.
24+
*
25+
* Validation failure will throw a `SerializationException` with a descriptive message indicating the
26+
* problem with the directory path.
27+
*
28+
* Associated factory implementation provides the actual validation logic.
29+
*/
30+
@Target(AnnotationTarget.FIELD)
31+
@Retention(AnnotationRetention.RUNTIME)
32+
annotation class Directory {
33+
companion object {
34+
internal object Factory : Constraint.Factory<Directory, Any?> {
35+
override fun make(data: Directory, type: Type): Constraint<Any?> = Constraint { value ->
36+
val path = value.asPathOrNull() ?: return@Constraint
37+
if (!path.exists() || !path.isDirectory()) {
38+
throw SerializationException("Path must point to an existing directory: $path")
39+
}
40+
}
41+
}
42+
}
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.slne.surf.api.core.config.constraints
2+
3+
import org.spongepowered.configurate.objectmapping.meta.Constraint
4+
import org.spongepowered.configurate.serialize.SerializationException
5+
import java.lang.reflect.Type
6+
7+
/**
8+
* Prevents specific values from being assigned to the annotated field.
9+
*
10+
* This annotation is used to define a set of disallowed string values for a field. If the annotated field's
11+
* value matches any of the specified disallowed values (case-insensitive), a `SerializationException` is thrown.
12+
*
13+
* @property values The array of string values that are disallowed.
14+
*/
15+
@Target(AnnotationTarget.FIELD)
16+
@Retention(AnnotationRetention.RUNTIME)
17+
annotation class DisallowValues(vararg val values: String) {
18+
companion object {
19+
internal object Factory : Constraint.Factory<DisallowValues, Any?> {
20+
override fun make(data: DisallowValues, type: Type): Constraint<Any?> = { value ->
21+
if (value != null && data.values.any { it.equals(value.toString(), ignoreCase = true) }) {
22+
throw SerializationException("Value '$value' is not allowed")
23+
}
24+
}
25+
}
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.slne.surf.api.core.config.constraints
2+
3+
import org.spongepowered.configurate.objectmapping.meta.Constraint
4+
import org.spongepowered.configurate.serialize.SerializationException
5+
import java.lang.reflect.Type
6+
7+
/**
8+
* Validates that a string config value ends with a specified suffix.
9+
*
10+
* This annotation ensures that the annotated string ends with the provided suffix.
11+
* If the validation fails, a `SerializationException` is thrown with a descriptive error message.
12+
*
13+
* @property suffix The suffix that the string value must end with.
14+
*/
15+
@Target(AnnotationTarget.FIELD)
16+
@Retention(AnnotationRetention.RUNTIME)
17+
annotation class EndsWith(val suffix: String) {
18+
companion object {
19+
internal object Factory : Constraint.Factory<EndsWith, String?> {
20+
override fun make(data: EndsWith, type: Type): Constraint<String?> = { value ->
21+
if (value != null && !value.endsWith(data.suffix)) {
22+
throw SerializationException("String must end with '${data.suffix}'")
23+
}
24+
}
25+
}
26+
}
27+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package dev.slne.surf.api.core.config.constraints
2+
3+
import org.spongepowered.configurate.objectmapping.meta.Constraint
4+
import org.spongepowered.configurate.serialize.SerializationException
5+
import java.io.File
6+
import java.lang.reflect.Type
7+
import java.nio.file.Path
8+
import kotlin.io.path.exists
9+
import kotlin.io.path.isRegularFile
10+
11+
/**
12+
* Requires that the annotated field refers to an existing file.
13+
*
14+
* This annotation ensures that the value of the annotated field corresponds to
15+
* a valid file path that exists and is a regular file. If the validation fails,
16+
* a `SerializationException` is thrown with a descriptive error message.
17+
*
18+
* The value can be:
19+
* - A `Path` object.
20+
* - A `File` object.
21+
* - A `String` representing the file path.
22+
*
23+
* If the value is not convertible to a file path or the file does not exist
24+
* or is not a regular file, the validation fails.
25+
*
26+
* This annotation is typically used for configuration values to ensure
27+
* that specified file paths are valid at runtime.
28+
*/
29+
@Target(AnnotationTarget.FIELD)
30+
@Retention(AnnotationRetention.RUNTIME)
31+
annotation class ExistingFile {
32+
companion object {
33+
internal object Factory : Constraint.Factory<ExistingFile, Any?> {
34+
override fun make(data: ExistingFile, type: Type): Constraint<Any?> = Constraint { value ->
35+
val path = value.asPathOrNull() ?: return@Constraint
36+
if (!path.exists() || !path.isRegularFile()) {
37+
throw SerializationException("Path must point to an existing file: $path")
38+
}
39+
}
40+
}
41+
}
42+
}
43+
44+
internal fun Any?.asPathOrNull(): Path? {
45+
return when (this) {
46+
null -> null
47+
is Path -> this
48+
is File -> toPath()
49+
is String -> runCatching { Path.of(this) }.getOrNull()
50+
else -> null
51+
}
52+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.slne.surf.api.core.config.constraints
2+
3+
import dev.slne.surf.api.core.config.type.ConfigDuration
4+
import org.spongepowered.configurate.objectmapping.meta.Constraint
5+
import org.spongepowered.configurate.serialize.SerializationException
6+
import java.lang.reflect.Type
7+
8+
/**
9+
* Annotation for constraining a `ConfigDuration`'s value to a maximum duration.
10+
*
11+
* This annotation ensures that the duration value of the annotated field does not exceed
12+
* the specified number of seconds. If the validation fails, a `SerializationException`
13+
* is thrown with a descriptive error message.
14+
*
15+
* @property seconds The maximum duration in seconds that the annotated field can have.
16+
*/
17+
@Target(AnnotationTarget.FIELD)
18+
@Retention(AnnotationRetention.RUNTIME)
19+
annotation class MaxDuration(val seconds: Long) {
20+
companion object {
21+
internal object Factory : Constraint.Factory<MaxDuration, ConfigDuration?> {
22+
override fun make(data: MaxDuration, type: Type): Constraint<ConfigDuration?> = { value ->
23+
if (value != null && value.value.inWholeSeconds > data.seconds) {
24+
throw SerializationException("Duration is too long: ${value.value}, expected <= ${data.seconds}s")
25+
}
26+
}
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)