aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/dev/isxander/yacl3/dsl
diff options
context:
space:
mode:
authorisXander <xander@isxander.dev>2024-04-14 23:19:21 +0100
committerisXander <xander@isxander.dev>2024-04-14 23:19:21 +0100
commit97bbc5a3d91ed57e55796777bbfc117ff28e2221 (patch)
tree3b9d17cb271a7676149d9d62bcbbe32bc72d4f9c /src/main/kotlin/dev/isxander/yacl3/dsl
parent26aec79e10025ff3427ceb47602156ebd670b2ac (diff)
downloadYetAnotherConfigLib-97bbc5a3d91ed57e55796777bbfc117ff28e2221.tar.gz
YetAnotherConfigLib-97bbc5a3d91ed57e55796777bbfc117ff28e2221.tar.bz2
YetAnotherConfigLib-97bbc5a3d91ed57e55796777bbfc117ff28e2221.zip
Add Kotlin DSL
Diffstat (limited to 'src/main/kotlin/dev/isxander/yacl3/dsl')
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/API.kt105
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt36
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt110
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt283
4 files changed, 534 insertions, 0 deletions
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt
new file mode 100644
index 0000000..87778e5
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt
@@ -0,0 +1,105 @@
+package dev.isxander.yacl3.dsl
+
+import dev.isxander.yacl3.api.*
+import net.minecraft.network.chat.Component
+
+interface YACLDsl {
+ val namespaceKey: String
+
+ val categories: YACLDslReference
+
+ fun title(component: Component)
+ fun title(block: () -> Component)
+
+ fun category(id: String, block: CategoryDsl.() -> Unit): ConfigCategory
+
+ fun save(block: () -> Unit)
+}
+
+interface OptionAddableDsl {
+ fun <T : Any> option(id: String, block: OptionDsl<T>.() -> Unit): Option<T>
+}
+
+interface CategoryDsl : OptionAddableDsl {
+ val groups: CategoryDslReference
+ val options: GroupDslReference
+
+ fun group(id: String, block: GroupDsl.() -> Unit): OptionGroup
+
+ fun name(component: Component)
+ fun name(block: () -> Component)
+
+ fun tooltip(vararg component: Component)
+ fun tooltipBuilder(block: TooltipBuilderDsl.() -> Unit)
+ fun useDefaultTooltip(lines: Int = 1)
+}
+
+interface GroupDsl : OptionAddableDsl {
+ val options: GroupDslReference
+
+ fun name(component: Component)
+ fun name(block: () -> Component)
+
+ fun descriptionBuilder(block: OptionDescription.Builder.() -> Unit)
+ fun description(description: OptionDescription)
+ fun useDefaultDescription(lines: Int = 1)
+}
+
+interface OptionDsl<T> : Option.Builder<T> {
+ val option: FutureValue<Option<T>>
+
+ fun OptionDescription.Builder.addDefaultDescription(lines: Int? = null)
+}
+
+interface TooltipBuilderDsl {
+ fun text(component: Component)
+ fun text(block: () -> Component)
+
+ operator fun Component.unaryPlus()
+
+ class Delegate(private val tooltipFunction: (Component) -> Unit) : TooltipBuilderDsl {
+ override fun text(component: Component) {
+ tooltipFunction(component)
+ }
+
+ override fun text(block: () -> Component) {
+ text(block())
+ }
+
+ override fun Component.unaryPlus() {
+ text(this)
+ }
+ }
+}
+
+interface YACLDslReference : Reference<CategoryDslReference> {
+ fun get(): YetAnotherConfigLib?
+
+ val isBuilt: Boolean
+
+ fun registering(block: CategoryDsl.() -> Unit): RegisterableDelegateProvider<CategoryDsl, ConfigCategory>
+}
+
+interface CategoryDslReference : Reference<GroupDslReference> {
+ fun get(): ConfigCategory?
+
+ val root: GroupDslReference
+
+ val isBuilt: Boolean
+
+ fun registering(block: GroupDsl.() -> Unit): RegisterableDelegateProvider<GroupDsl, OptionGroup>
+}
+
+interface GroupDslReference {
+ fun get(): OptionGroup?
+
+ operator fun <T> get(id: String): FutureValue<Option<T>>
+
+ val isBuilt: Boolean
+
+ fun <T : Any> registering(block: OptionDsl<T>.() -> Unit): RegisterableDelegateProvider<OptionDsl<T>, Option<T>>
+}
+
+
+
+
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt
new file mode 100644
index 0000000..4b93f5f
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt
@@ -0,0 +1,36 @@
+package dev.isxander.yacl3.dsl
+
+import dev.isxander.yacl3.api.Option
+import dev.isxander.yacl3.api.OptionDescription
+import dev.isxander.yacl3.api.OptionGroup
+import dev.isxander.yacl3.api.controller.ControllerBuilder
+import net.minecraft.network.chat.Component
+import kotlin.reflect.KMutableProperty0
+
+fun <T : Any> Option.Builder<T>.binding(property: KMutableProperty0<T>, default: T) {
+ binding(default, { property.get() }, { property.set(it) })
+}
+
+fun <T : Any> Option.Builder<T>.descriptionBuilder(block: OptionDescription.Builder.(T) -> Unit) {
+ description { OptionDescription.createBuilder().apply { block(it) }.build() }
+}
+
+fun Option.Builder<*>.descriptionBuilderConst(block: OptionDescription.Builder.() -> Unit) {
+ description(OptionDescription.createBuilder().apply(block).build())
+}
+
+fun Option.Builder<*>.available(block: () -> Boolean) {
+ available(block())
+}
+
+fun OptionDescription.Builder.text(block: () -> Component) {
+ text(block())
+}
+
+fun OptionGroup.Builder.descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
+ description(OptionDescription.createBuilder().apply(block).build())
+}
+
+fun <T, B : ControllerBuilder<T>> Option.Builder<T>.controller(builder: (Option<T>) -> B, block: B.() -> Unit = {}) {
+ controller { builder(it).apply(block) }
+}
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt
new file mode 100644
index 0000000..819365c
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt
@@ -0,0 +1,110 @@
+package dev.isxander.yacl3.dsl
+
+import dev.isxander.yacl3.api.Option
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+interface FutureValue<T> {
+ fun onReady(block: (T) -> Unit)
+ fun <R> map(block: (T) -> R): FutureValue<R>
+ fun <R> flatMap(block: (T) -> FutureValue<R>): FutureValue<R>
+ fun getOrNull(): T?
+ fun getOrThrow(): T = getOrNull() ?: error("Value not ready yet!")
+
+ open class Impl<T>(default: T? = null) : FutureValue<T> {
+ var value: T? = default
+ set(value) {
+ field = value
+ while (taskQueue.isNotEmpty()) {
+ taskQueue.removeFirst()(value!!)
+ }
+ }
+ private val taskQueue = ArrayDeque<(T) -> Unit>()
+
+ override fun onReady(block: (T) -> Unit) {
+ if (value != null) block(value!!)
+ else taskQueue.add(block)
+ }
+
+ override fun <R> map(block: (T) -> R): FutureValue<R> {
+ val future = Impl<R>()
+ onReady {
+ future.value = block(it)
+ }
+ return future
+ }
+
+ override fun <R> flatMap(block: (T) -> FutureValue<R>): FutureValue<R> {
+ val future = Impl<R>()
+ onReady {
+ block(it).onReady { inner ->
+ future.value = inner
+ }
+ }
+ return future
+ }
+
+ override fun getOrNull(): T? = value
+ }
+}
+
+interface Reference<T> : ReadOnlyProperty<Any?, FutureValue<T>> {
+ operator fun get(id: String): FutureValue<T>
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): FutureValue<T> {
+ return get(property.name)
+ }
+
+ operator fun invoke(name: String? = null, block: (T) -> Unit): ReadOnlyProperty<Any?, FutureValue<T>> {
+ return ReadOnlyProperty { thisRef, property ->
+ val future = get(name ?: property.name)
+ future.onReady(block)
+ future
+ }
+ }
+}
+
+
+operator fun <T> FutureValue<out Reference<T>>.get(id: String): FutureValue<T> {
+ val future = FutureValue.Impl<FutureValue<T>>()
+ onReady {
+ future.value = it[id]
+ }
+ return future.flatten()
+}
+
+fun FutureValue<GroupDslReference>.getOption(id: String): FutureValue<Option<*>> {
+ val future = FutureValue.Impl<FutureValue<Option<*>>>()
+ onReady {
+ future.value = it.get<Any?>(id) as FutureValue<Option<*>>
+ }
+ return future.flatten()
+}
+
+
+private fun <T> FutureValue<FutureValue<T>>.flatten(): FutureValue<T> {
+ val future = FutureValue.Impl<T>()
+ onReady { outer ->
+ outer.onReady { inner ->
+ future.value = inner
+ }
+ }
+ return future
+}
+
+class RegisterableDelegateProvider<Dsl, Return>(
+ private val registerFunction: (String, Dsl.() -> Unit) -> Return,
+ private val action: Dsl.() -> Unit
+) {
+ operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ExistingDelegateProvider<Return> {
+ return ExistingDelegateProvider(registerFunction(property.name, action))
+ }
+}
+
+class ExistingDelegateProvider<Return>(
+ private val delegate: Return
+) {
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): Return {
+ return delegate
+ }
+}
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt
new file mode 100644
index 0000000..8c10cfd
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt
@@ -0,0 +1,283 @@
+package dev.isxander.yacl3.dsl
+
+import dev.isxander.yacl3.api.*
+import net.minecraft.locale.Language
+import net.minecraft.network.chat.Component
+
+fun YetAnotherConfigLib(namespace: String, block: YACLDsl.() -> Unit): YetAnotherConfigLib {
+ val context = YACLDslContext(namespace)
+ context.block()
+ return context.build()
+}
+
+class YACLDslContext(
+ private val namespace: String,
+ private val builder: YetAnotherConfigLib.Builder = YetAnotherConfigLib.createBuilder()
+) : YACLDsl {
+ private val categoryMap = LinkedHashMap<String, YACLDslCategoryContext>()
+ private val categoryDslReferenceMap = mutableMapOf<String, FutureValue.Impl<CategoryDslReference>>()
+
+ override val namespaceKey = "yacl3.config.$namespace"
+
+ private var used = false
+ private var built: YetAnotherConfigLib? = null
+
+ private var saveFunction: () -> Unit = {}
+
+ override val categories = object : YACLDslReference {
+ override fun get(): YetAnotherConfigLib? = built
+
+ override operator fun get(id: String): FutureValue<CategoryDslReference> =
+ FutureValue.Impl(categoryMap[id]?.groups).also { categoryDslReferenceMap[id] = it }
+
+ override fun registering(block: CategoryDsl.() -> Unit): RegisterableDelegateProvider<CategoryDsl, ConfigCategory> {
+ return RegisterableDelegateProvider({ id, configuration -> category(id, configuration) }, block)
+ }
+
+ override val isBuilt: Boolean
+ get() = built != null
+ }
+
+ init {
+ title(Component.translatable("$namespaceKey.title"))
+ }
+
+ override fun title(component: Component) {
+ builder.title(component)
+ }
+
+ override fun title(block: () -> Component) {
+ title(block())
+ }
+
+ override fun category(id: String, block: CategoryDsl.() -> Unit): ConfigCategory {
+ val context = YACLDslCategoryContext(id, this)
+ context.block()
+ categoryMap[id] = context
+ categoryDslReferenceMap[id]?.value = context.groups
+
+ val built = context.build()
+ builder.category(built)
+
+ return built
+ }
+
+ override fun save(block: () -> Unit) {
+ val oldSaveFunction = saveFunction
+ saveFunction = { // allows stacking of save functions
+ oldSaveFunction()
+ block()
+ }
+ }
+
+ fun build(): YetAnotherConfigLib {
+ if (used) error("Cannot use the same DSL context twice!")
+ used = true
+
+ builder.save(saveFunction)
+
+ return builder.build().also { built = it }
+ }
+}
+
+class YACLDslCategoryContext(
+ private val id: String,
+ private val root: YACLDslContext,
+ private val builder: ConfigCategory.Builder = ConfigCategory.createBuilder(),
+) : CategoryDsl {
+ private val groupMap = LinkedHashMap<String, YACLDslGroupContext>()
+ private val groupDslReferenceMap = mutableMapOf<String, FutureValue.Impl<GroupDslReference>>()
+ val categoryKey = "${root.namespaceKey}.$id"
+
+ private var built: ConfigCategory? = null
+
+ private val rootGroup: YACLDslGroupContext = YACLDslGroupContext(id, this, builder.rootGroupBuilder(), root = true)
+
+ override val groups = object : CategoryDslReference {
+ override fun get(): ConfigCategory? = built
+
+ override fun get(id: String): FutureValue<GroupDslReference> =
+ FutureValue.Impl(groupMap[id]?.options).also { groupDslReferenceMap[id] = it }
+
+ override val root: GroupDslReference
+ get() = rootGroup.options
+
+ override fun registering(block: GroupDsl.() -> Unit): RegisterableDelegateProvider<GroupDsl, OptionGroup> {
+ return RegisterableDelegateProvider({ id, configuration -> group(id, configuration) }, block)
+ }
+
+ override val isBuilt: Boolean
+ get() = built != null
+
+ }
+
+ override val options = rootGroup.options
+
+ init {
+ builder.name(Component.translatable("$categoryKey.title"))
+ }
+
+ override fun name(component: Component) {
+ builder.name(component)
+ }
+
+ override fun name(block: () -> Component) {
+ name(block())
+ }
+
+ override fun group(id: String, block: GroupDsl.() -> Unit): OptionGroup {
+ val context = YACLDslGroupContext(id, this)
+ context.block()
+ groupMap[id] = context
+ groupDslReferenceMap[id]?.value = context.options
+
+ return context.build().also {
+ builder.group(it)
+ }
+ }
+
+ override fun <T : Any> option(id: String, block: OptionDsl<T>.() -> Unit): Option<T> =
+ rootGroup.option(id, block)
+
+ override fun tooltip(vararg component: Component) {
+ builder.tooltip(*component)
+ }
+
+ override fun tooltipBuilder(block: TooltipBuilderDsl.() -> Unit) {
+ val builder = TooltipBuilderDsl.Delegate { builder.tooltip(it) }
+ builder.block()
+ }
+
+ override fun useDefaultTooltip(lines: Int) {
+ if (lines == 1) {
+ builder.tooltip(Component.translatable("$categoryKey.tooltip"))
+ } else for (i in 1..lines) {
+ builder.tooltip(Component.translatable("$categoryKey.tooltip.$i"))
+ }
+ }
+
+ fun build(): ConfigCategory {
+ return builder.build().also { built = it }
+ }
+}
+
+class YACLDslGroupContext(
+ private val id: String,
+ private val category: YACLDslCategoryContext,
+ private val builder: OptionGroup.Builder = OptionGroup.createBuilder(),
+ private val root: Boolean = false,
+) : GroupDsl {
+ private val optionMap = LinkedHashMap<String, YACLDslOptionContext<*>>()
+ private val optionDslReferenceMap = mutableMapOf<String, FutureValue.Impl<Option<*>>>()
+ val groupKey = "${category.categoryKey}.$id"
+ private var built: OptionGroup? = null
+
+ override val options = object : GroupDslReference {
+ override fun get(): OptionGroup? = built
+
+ override fun <T> get(id: String): FutureValue<Option<T>> =
+ FutureValue.Impl(optionMap[id]).flatMap { it.option as FutureValue<Option<T>> }.also { optionDslReferenceMap[id] = it as FutureValue.Impl<Option<*>> }
+
+ override fun <T : Any> registering(block: OptionDsl<T>.() -> Unit): RegisterableDelegateProvider<OptionDsl<T>, Option<T>> {
+ return RegisterableDelegateProvider({ id, configuration -> option(id, configuration) }, block)
+ }
+
+ override val isBuilt: Boolean
+ get() = built != null
+
+ }
+
+ override fun name(component: Component) {
+ builder.name(component)
+ }
+
+ override fun name(block: () -> Component) {
+ name(block())
+ }
+
+ override fun descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
+ builder.description(OptionDescription.createBuilder().apply(block).build())
+ }
+
+ override fun description(description: OptionDescription) {
+ builder.description(description)
+ }
+
+ init {
+ if (!root) {
+ builder.name(Component.translatable("$groupKey.name"))
+ }
+ }
+
+ override fun <T : Any> option(id: String, block: OptionDsl<T>.() -> Unit): Option<T> {
+ val context = YACLDslOptionContext<T>(id, this)
+ context.block()
+ optionMap[id] = context
+
+ return context.build().also {
+ optionDslReferenceMap[id]?.value = it
+ builder.option(it)
+ }
+ }
+
+ override fun useDefaultDescription(lines: Int) {
+ descriptionBuilder {
+ if (lines == 1) {
+ text(Component.translatable("$groupKey.description"))
+ } else for (i in 1..lines) {
+ text(Component.translatable("$groupKey.description.$i"))
+ }
+ }
+ }
+
+ fun build(): OptionGroup {
+ return builder.build().also { built = it }
+ }
+}
+
+class YACLDslOptionContext<T : Any>(
+ private val id: String,
+ private val group: YACLDslGroupContext,
+ private val builder: Option.Builder<T> = Option.createBuilder()
+) : Option.Builder<T> by builder, OptionDsl<T> {
+ val optionKey = "${group.groupKey}.$id"
+ private var built: Option<T>? = null
+
+ private val taskQueue = ArrayDeque<(Option<T>) -> Unit>()
+ override val option = FutureValue.Impl<Option<T>>()
+
+ init {
+ name(Component.translatable("$optionKey.name"))
+ }
+
+ override fun OptionDescription.Builder.addDefaultDescription(lines: Int?) {
+ if (lines != null) {
+ if (lines == 1) {
+ text(Component.translatable("$optionKey.description"))
+ } else for (i in 1..lines) {
+ text(Component.translatable("$optionKey.description.$i"))
+ }
+ } else {
+ // loop until we find a key that doesn't exist
+ var i = 1
+ while (i < 100) {
+ val key = "$optionKey.description.$i"
+ if (Language.getInstance().has(key)) {
+ text(Component.translatable(key))
+ }
+
+ i++
+ }
+ }
+ }
+
+ override fun build(): Option<T> {
+ return builder.build().also {
+ built = it
+ option.value = it
+ while (taskQueue.isNotEmpty()) {
+ taskQueue.removeFirst()(it)
+ }
+ }
+ }
+}