diff options
author | isXander <xander@isxander.dev> | 2024-04-14 23:19:21 +0100 |
---|---|---|
committer | isXander <xander@isxander.dev> | 2024-04-14 23:19:21 +0100 |
commit | 97bbc5a3d91ed57e55796777bbfc117ff28e2221 (patch) | |
tree | 3b9d17cb271a7676149d9d62bcbbe32bc72d4f9c /src/main/kotlin/dev/isxander/yacl3/dsl | |
parent | 26aec79e10025ff3427ceb47602156ebd670b2ac (diff) | |
download | YetAnotherConfigLib-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.kt | 105 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt | 36 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt | 110 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt | 283 |
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) + } + } + } +} |