Skip to content

Commit f4ae03b

Browse files
committed
refactor: Config system rework
feat: scopedConfig helper in features
1 parent 939a17f commit f4ae03b

File tree

14 files changed

+323
-190
lines changed

14 files changed

+323
-190
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ try to keep MAJOR changes in sync with Minecraft updates.
1717
### Changed
1818

1919
- **Feature system** has been reworked as a DSL built around Koin for dependency injection
20+
- **Config system** has been reworked to use a builder class that can then pick whether to decode a single file, directory, or multi-entry format which has been moved here from Geary
2021
- **SerializableItemStack** uses a new service for letting other plugins register custom item types instead of manually adding support for them
2122
- Brigadier **command api** internals reworked with much cleaner inside logic and fixes to expected behaviour from Brigadier
2223

Lines changed: 55 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,70 @@
11
package com.mineinabyss.idofront.config
22

3+
import com.charleskorn.kaml.Yaml
34
import kotlinx.serialization.KSerializer
5+
import kotlinx.serialization.StringFormat
6+
import kotlinx.serialization.json.Json
47
import java.nio.file.Path
5-
import kotlin.io.path.*
6-
import kotlin.properties.ReadWriteProperty
7-
import kotlin.reflect.KProperty
8+
import kotlin.io.path.createFile
9+
import kotlin.io.path.createParentDirectories
10+
import kotlin.io.path.inputStream
11+
import kotlin.io.path.notExists
12+
import kotlin.io.path.outputStream
813

9-
@Deprecated("Use new config api instead")
10-
class Config<T>(
11-
val name: String,
12-
val path: Path,
13-
val default: T,
14+
/**
15+
* Config decoding definition, built via [ConfigBuilder].
16+
*
17+
* Can be used to read/write a config from a single path, or configs from a directory.
18+
*/
19+
data class Config<T>(
1420
val serializer: KSerializer<T>,
15-
val preferredFormat: Format,
16-
val formats: ConfigFormats,
17-
val mergeUpdates: Boolean,
18-
val lazyLoad: Boolean,
19-
val onFirstLoad: (T) -> Unit = {},
20-
val onReload: (T) -> Unit = {},
21-
val onLoad: (T) -> Unit = {},
22-
) : ReadWriteProperty<Any?, T> {
23-
private var loaded: T? = null
24-
private val fileFormat = checkFormat()
25-
private val fileName = "$name.${fileFormat.ext}"
26-
private val configFile = (path / fileName).createParentDirectories()
21+
val format: StringFormat,
22+
val default: T?,
23+
val writeBack: WriteMode,
24+
) {
25+
/**
26+
* Read and write a single config file located at [path]
27+
*
28+
* @see SingleConfig
29+
*/
30+
fun single(path: Path) = SingleConfig(this, path)
2731

28-
init {
29-
if (!lazyLoad) getOrLoad()
30-
}
32+
/**
33+
* Read and write many config files located in a directory at [path].
34+
*
35+
* @see DirectoryConfig
36+
*/
37+
fun fromDirectory(
38+
path: Path,
39+
extension: String = inferExtensionFromFormat(),
40+
): DirectoryConfig<T> = DirectoryConfig(this, path, extension)
3141

32-
private fun checkFormat(): Format {
33-
return formats.formats.firstOrNull {
34-
val file = path / "$name.${it.ext}"
35-
file.exists()
36-
} ?: preferredFormat
42+
/**
43+
* Read and write many config files in a directory at [path], where each file contains
44+
* key-value pairs of strings to this config type [T].
45+
*
46+
* Each entry may also reuse parts of others using an `include` key that will be parsed
47+
* automatically, without the underlying serializer needing to know about it.
48+
*
49+
* @throws IllegalArgumentException if the [format] is not [Yaml].
50+
* @see MultiEntryYamlReader
51+
*/
52+
fun multiEntry(path: Path): MultiEntryYamlReader<T> {
53+
return MultiEntryYamlReader(this, path, format as? Yaml ?: error("Format must be YAML"))
3754
}
3855

39-
fun getOrLoad(): T {
40-
loaded?.let { return it }
41-
return runCatching(::load).onFailure { it.printStackTrace() }.getOrDefault(loaded ?: default).also(onFirstLoad).also(onLoad)
56+
internal fun decode(path: Path): Result<T> = runCatching {
57+
SerializationHelpers.decode(format, serializer, path.inputStream())
4258
}
4359

44-
fun reload(): T {
45-
return runCatching(::load).onFailure { it.printStackTrace() }.getOrDefault(loaded ?: default).also(onReload).also(onLoad)
60+
internal fun encode(path: Path, data: T) = runCatching {
61+
if (path.notExists()) path.createParentDirectories().createFile()
62+
SerializationHelpers.encode(format, serializer, path.outputStream(), data)
4663
}
4764

48-
private fun load(): T {
49-
val decoded = when {
50-
configFile.exists() && configFile.readText().isNotEmpty() -> {
51-
configFile.inputStream().use { stream ->
52-
formats.decode(
53-
preferredFormat.stringFormat,
54-
serializer,
55-
stream
56-
)
57-
}
58-
}
59-
60-
else -> {
61-
configFile.toFile().createNewFile()
62-
default
63-
}
64-
}
65-
66-
// Merge with any new changes
67-
if (mergeUpdates) write(decoded)
68-
return decoded.also { loaded = it }
65+
internal fun inferExtensionFromFormat() = when (format) {
66+
is Yaml -> "yml"
67+
is Json -> "json"
68+
else -> error("Could not infer extension from unknown format $format")
6969
}
70-
71-
fun write(data: T) {
72-
configFile.outputStream().use { stream ->
73-
formats.encode(
74-
fileFormat.stringFormat,
75-
serializer,
76-
stream,
77-
data
78-
)
79-
}
80-
}
81-
82-
override fun getValue(thisRef: Any?, property: KProperty<*>): T = getOrLoad()
83-
84-
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = write(value)
85-
}
70+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.mineinabyss.idofront.config
2+
3+
import com.charleskorn.kaml.Yaml
4+
import com.charleskorn.kaml.YamlConfiguration
5+
import kotlinx.serialization.KSerializer
6+
import kotlinx.serialization.StringFormat
7+
import kotlinx.serialization.json.Json
8+
import kotlinx.serialization.modules.SerializersModule
9+
import kotlinx.serialization.modules.plus
10+
11+
/**
12+
* Builder for a [Config] definition.
13+
*
14+
* @see config
15+
*/
16+
data class ConfigBuilder<T>(
17+
/** Serializer to use for decoding the config file. */
18+
var serializer: KSerializer<T>,
19+
/** Format to use for decoding the config file. */
20+
var format: StringFormat = Yaml(
21+
configuration = YamlConfiguration(
22+
strictMode = false
23+
)
24+
),
25+
) {
26+
/** Defines when the decoded config file should be written back to (ex. to add new fields, or encode a default config.) */
27+
var writeBack: WriteMode = WriteMode.WHEN_EMPTY
28+
29+
/** Provide a default config to use if decoding fails. Only applicable when using as [SingleConfig]. */
30+
var default: T? = null
31+
32+
/**
33+
* Helper to add a [SerializersModule] to the [StringFormat] being used to decode the config.
34+
*
35+
* Note: only supports [Yaml] and [Json] formats, for others, manually set the [format].
36+
*/
37+
fun withSerializersModule(module: SerializersModule) {
38+
format = when (val format = format) {
39+
is Yaml -> Yaml(format.serializersModule + module, format.configuration)
40+
is Json -> Json(format) { serializersModule += module }
41+
else -> error("Could not automatically add serializers module to unknown format $format, manually add it instead.")
42+
}
43+
}
44+
45+
fun build() = Config(
46+
serializer = serializer,
47+
format = format,
48+
writeBack = writeBack,
49+
default = default,
50+
)
51+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.mineinabyss.idofront.config
2+
3+
import java.nio.file.Path
4+
5+
data class ConfigEntry<T>(val path: Path, val data: T)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.mineinabyss.idofront.config
2+
3+
import java.nio.file.Path
4+
5+
data class ConfigEntryWithKey<T>(
6+
val path: Path,
7+
val key: String,
8+
val entry: T,
9+
)

idofront-config/src/main/kotlin/com/mineinabyss/idofront/config/ConfigFormats.kt

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,18 @@
11
package com.mineinabyss.idofront.config
22

3-
import com.charleskorn.kaml.*
3+
import com.charleskorn.kaml.Yaml
4+
import com.charleskorn.kaml.decodeFromStream
5+
import com.charleskorn.kaml.encodeToStream
46
import kotlinx.serialization.DeserializationStrategy
57
import kotlinx.serialization.SerializationStrategy
68
import kotlinx.serialization.StringFormat
79
import kotlinx.serialization.json.Json
810
import kotlinx.serialization.json.decodeFromStream
911
import kotlinx.serialization.json.encodeToStream
10-
import kotlinx.serialization.modules.EmptySerializersModule
11-
import kotlinx.serialization.modules.SerializersModule
1212
import java.io.InputStream
1313
import java.io.OutputStream
1414

15-
@Deprecated("Use new config api instead")
16-
open class ConfigFormats(
17-
overrides: List<Format> = listOf(),
18-
val serializersModule: SerializersModule = EmptySerializersModule(),
19-
) {
20-
private val defaultFormats = listOf(
21-
Format(
22-
"yml", Yaml(
23-
serializersModule = serializersModule,
24-
YamlConfiguration(
25-
encodeDefaults = true,
26-
strictMode = false,
27-
sequenceBlockIndent = 2,
28-
singleLineStringStyle = SingleLineStringStyle.PlainExceptAmbiguous
29-
)
30-
)
31-
),
32-
Format("json", Json {
33-
encodeDefaults = true
34-
ignoreUnknownKeys = true
35-
prettyPrint = true
36-
serializersModule = this@ConfigFormats.serializersModule
37-
})
38-
)
39-
40-
val extToFormat = defaultFormats.associateBy { it.ext }
41-
.plus(overrides.associateBy { it.ext })
42-
43-
val formats = extToFormat.values.toList()
44-
15+
internal object SerializationHelpers {
4516
fun <T> decode(format: StringFormat, serializer: DeserializationStrategy<T>, input: InputStream): T =
4617
when (format) {
4718
is Yaml -> format.decodeFromStream(serializer, input)
Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,10 @@
11
package com.mineinabyss.idofront.config
22

3-
import com.charleskorn.kaml.Yaml
4-
import kotlinx.serialization.KSerializer
5-
import kotlinx.serialization.StringFormat
6-
import kotlinx.serialization.modules.SerializersModule
7-
import java.nio.file.Path
3+
import kotlinx.serialization.serializer
84

9-
inline fun <reified T> config(block: ConfigBuilder<T>.() -> Unit): ConfigManager<T> {
10-
return TODO() //ConfigBuilder(serializer<T>(), format)
5+
/**
6+
* Entrypoint for Idofront's config helpers.
7+
*/
8+
inline fun <reified T> config(block: ConfigBuilder<T>.() -> Unit = {}): Config<T> {
9+
return ConfigBuilder(serializer<T>()).apply(block).build()
1110
}
12-
13-
data class ConfigBuilder<T>(
14-
var serializer: KSerializer<T>,
15-
var format: StringFormat
16-
) {
17-
var serializersModule: SerializersModule? = null
18-
}
19-
20-
class ConfigManager<T> {
21-
private val defaultPath: Path? = null
22-
23-
fun single(path: Path) {
24-
25-
}
26-
27-
fun directory(path: Path): List<ConfigEntry<T>> {
28-
TODO()
29-
}
30-
// fun read(path: Path = defaultPath ?: error("No default path specified")): T = TODO()
31-
//
32-
// fun write(value: T, path: Path = defaultPath ?: error("No default path specified")) {
33-
//
34-
// }
35-
36-
fun readFromDirectory(path: Path): List<ConfigEntry<T>> = TODO()
37-
}
38-
39-
data class ConfigEntry<T>(val path: Path, val data: T)
40-
41-
data class Test(
42-
val a: String
43-
)
44-
fun main() {
45-
config<Test> {
46-
format = Yaml.default
47-
}.single(Path.of("test.yml"))
48-
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.mineinabyss.idofront.config
2+
3+
import com.mineinabyss.idofront.messaging.idofrontLogger
4+
import java.nio.file.Path
5+
import kotlin.io.path.extension
6+
import kotlin.io.path.isRegularFile
7+
import kotlin.io.path.walk
8+
9+
class DirectoryConfig<T>(
10+
val config: Config<T>,
11+
val directory: Path,
12+
val extension: String,
13+
) {
14+
fun read(): List<ConfigEntry<T>> = directory.walk()
15+
.filter { it.isRegularFile() && it.extension == extension }
16+
.mapNotNull { path ->
17+
val decoded = config.decode(path)
18+
.onFailure {
19+
idofrontLogger.e { "Failed to read config file at $path" }
20+
it.printStackTrace()
21+
}
22+
.getOrNull()
23+
if (decoded != null) ConfigEntry<T>(path, decoded) else null
24+
}
25+
.toList()
26+
27+
fun readSingle(path: Path): T {
28+
return SingleConfig(config, path).read()
29+
}
30+
31+
fun writeSingle(data: T, path: Path) = SingleConfig(config, path).write(data)
32+
}

idofront-config/src/main/kotlin/com/mineinabyss/idofront/config/Format.kt

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)