Improved config handling, updated read-only and list configs

- The read-only configs are no longer using a separate class, it seems
unnecessary
- The list configs got refactored so that the entire list doesn't get
recreated each time it changes
- Also added support for getters/setters for the list config, just to be
consistent
This commit is contained in:
Norbi Peti 2023-03-17 16:45:38 +01:00
parent 99958e74f9
commit 8d8708d14b
5 changed files with 240 additions and 294 deletions

View file

@ -2,6 +2,7 @@ package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin import buttondevteam.core.MainPlugin
import buttondevteam.lib.ChromaUtils import buttondevteam.lib.ChromaUtils
import buttondevteam.lib.architecture.config.IConfigData
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.configuration.Configuration import org.bukkit.configuration.Configuration
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask
@ -14,14 +15,14 @@ import java.util.function.Function
* @param getter The parameter is of a primitive type as returned by [Configuration.get] * @param getter The parameter is of a primitive type as returned by [Configuration.get]
* @param setter The result should be a primitive type or string that can be retrieved correctly later * @param setter The result should be a primitive type or string that can be retrieved correctly later
*/ */
open class ConfigData<T> internal constructor( class ConfigData<T> internal constructor(
private val config: IHaveConfig?, val config: IHaveConfig?,
val path: String, override val path: String,
def: T?,
primitiveDef: Any?, primitiveDef: Any?,
private val getter: Function<Any?, T>, private val getter: Function<Any?, T>,
private val setter: Function<T, Any?> private val setter: Function<T, Any?>,
) { private val readOnly: Boolean
) : IConfigData<T> {
private val pdef: Any? private val pdef: Any?
/** /**
@ -30,7 +31,7 @@ open class ConfigData<T> internal constructor(
private var value: T? = null private var value: T? = null
init { init {
this.pdef = primitiveDef ?: def?.let { setter.apply(it) } this.pdef = primitiveDef
?: throw IllegalArgumentException("Either def or primitiveDef must be set. A getter and setter must be present when using primitiveDef.") ?: throw IllegalArgumentException("Either def or primitiveDef must be set. A getter and setter must be present when using primitiveDef.")
get() //Generate config automatically get() //Generate config automatically
} }
@ -39,11 +40,11 @@ open class ConfigData<T> internal constructor(
return "ConfigData{path='$path', value=$value}" return "ConfigData{path='$path', value=$value}"
} }
fun reset() { override fun reset() {
value = null value = null
} }
fun get(): T? { override fun get(): T? {
if (value != null) return value //Speed things up if (value != null) return value //Speed things up
val config = config?.config val config = config?.config
var `val`: Any? var `val`: Any?
@ -65,8 +66,8 @@ open class ConfigData<T> internal constructor(
return getter.apply(convert(`val`, pdef)).also { value = it } return getter.apply(convert(`val`, pdef)).also { value = it }
} }
fun set(value: T?) { override fun set(value: T?) {
if (this is ReadOnlyConfigData<*>) return //Safety for Discord channel/role data if (readOnly) return //Safety for Discord channel/role data
val `val` = value?.let { setter.apply(value) } val `val` = value?.let { setter.apply(value) }
setInternal(`val`) setInternal(`val`)
this.value = value this.value = value
@ -80,88 +81,30 @@ open class ConfigData<T> internal constructor(
private class SaveTask(val task: BukkitTask, val saveAction: Runnable) private class SaveTask(val task: BukkitTask, val saveAction: Runnable)
class ConfigDataBuilder<T> internal constructor(private val config: IHaveConfig, private val path: String) { class ConfigDataBuilder<T> internal constructor(
private var def: T? = null private val config: IHaveConfig,
private var primitiveDef: Any? = null private val path: String,
private val primitiveDef: Any?,
@Suppress("UNCHECKED_CAST") private val getter: Function<Any?, T>,
private var getter: Function<Any?, T> = Function { it as T } private val setter: Function<T, Any?>
private var setter: Function<T, Any?> = Function { it } ) {
/**
* The default value to use, as used in code. If not a primitive type, use the [.getter] and [.setter] methods.
* <br></br>
* To set the value as it is stored, use [.primitiveDef].
*
* @param def The default value
* @return This builder
*/
fun def(def: T): ConfigDataBuilder<T> {
this.def = def
return this
}
/**
* The default value to use, as stored in yaml. Must be a primitive type. Make sure to use the [.getter] and [.setter] methods.
* <br></br>
* To set the value as used in the code, use [.def].
*
* @param primitiveDef The default value
* @return This builder
*/
fun primitiveDef(primitiveDef: Any?): ConfigDataBuilder<T> {
this.primitiveDef = primitiveDef
return this
}
/**
* A function to use to obtain the runtime object from the yaml representation (usually string).
* The [.setter] must also be set.
*
* @param getter A function that receives the primitive type and returns the runtime type
* @return This builder
*/
fun getter(getter: Function<Any?, T>): ConfigDataBuilder<T> {
this.getter = getter
return this
}
/**
* A function to use to obtain the yaml representation (usually string) from the runtime object.
* The [.getter] must also be set.
*
* @param setter A function that receives the runtime type and returns the primitive type
* @return This builder
*/
fun setter(setter: Function<T, Any?>): ConfigDataBuilder<T> {
this.setter = setter
return this
}
/** /**
* Builds a modifiable config representation. Use if you want to change the value *in code*. * Builds a modifiable config representation. Use if you want to change the value *in code*.
* *
* @return A ConfigData instance. * @return A ConfigData instance.
*/ */
fun build(): ConfigData<T> { fun build(readOnly: Boolean = false): ConfigData<T> {
val config = ConfigData(config, path, def, primitiveDef, getter, setter) val config = ConfigData(config, path, primitiveDef, getter, setter, readOnly)
this.config.onConfigBuild(config) this.config.onConfigBuild(config)
return config return config
} }
/** fun buildList(readOnly: Boolean = false): ListConfigData<T> {
* Builds a read-only config representation. Use if you only want the value to be changed *in the config*. if (primitiveDef is List<*>) {
* val config = ListConfigData(config, path, primitiveDef, getter, setter, readOnly)
* @return A ReadOnlyConfigData instance. this.config.onConfigBuild(config)
*/ return config
fun buildReadOnly(): ReadOnlyConfigData<T> { }
val config = ReadOnlyConfigData(config, path, def, primitiveDef, getter, setter)
this.config.onConfigBuild(config)
return config
}
override fun toString(): String {
return "ConfigData.ConfigDataBuilder(config=$config, path=$path, def=$def, primitiveDef=$primitiveDef, getter=$getter, setter=$setter)"
} }
} }

View file

@ -3,6 +3,7 @@ package buttondevteam.lib.architecture
import buttondevteam.core.MainPlugin import buttondevteam.core.MainPlugin
import buttondevteam.lib.TBMCCoreAPI import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.ConfigData.ConfigDataBuilder import buttondevteam.lib.architecture.ConfigData.ConfigDataBuilder
import buttondevteam.lib.architecture.config.IConfigData
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.configuration.ConfigurationSection import org.bukkit.configuration.ConfigurationSection
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@ -16,90 +17,72 @@ import java.util.stream.Collectors
/** /**
* A config system * A config system
* May be used in testing. * May be used in testing.
*
* @param saveAction What to do to save the config to disk. Don't use get methods until it's non-null.
*/ */
class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after adding component builder class IHaveConfig(
private val datamap = HashMap<String, ConfigData<*>>() /**
* The way the underlying configuration gets saved to disk
*/
val saveAction: Runnable,
/** /**
* Returns the Bukkit ConfigurationSection. Use [.signalChange] after changing it. * Returns the Bukkit ConfigurationSection. Use [.signalChange] after changing it.
*/ */
var config: ConfigurationSection? = null // TODO: Make non-nullable after removing reset() method val config: ConfigurationSection
) {
private val datamap = HashMap<String, IConfigData<*>>()
/** /**
* Gets a config object for the given path. The def or primitiveDef must be set. If a getter is present, a setter must be present as well. * You may use this method with any data type, but always provide getters and setters that convert to primitive types
* if you're not using primitive types directly.
* These primitive types are strings, numbers, characters, booleans and lists of these things.
* *
* @param path The dot-separated path relative to this config instance * @param path The path in config to use
* @param <T> The runtime type of the config value * @param def The value to use by default
* @return A ConfigData builder to set how to obtain the value * @param getter A function that converts a primitive representation to the correct value
</T> */ * @param setter A function that converts a value to a primitive representation
fun <T> getConfig(path: String?): ConfigDataBuilder<T> { * @param primDef Whether the default value is a primitive value that needs to be converted to the correct type using the getter
* @param T The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
*/
fun <T> getConfig(
path: String,
def: T,
getter: Function<Any?, T>? = null,
setter: Function<T, Any?>? = null
): ConfigDataBuilder<T> {
return ConfigData.builder(this, path) return ConfigData.builder(this, path)
} }
fun onConfigBuild(config: ConfigData<*>) { fun onConfigBuild(config: IConfigData<*>) {
datamap[config.path] = config datamap[config.path] = config
} }
/** /**
* This method overload should only be used with primitives or String. * You may use this method with any data type, but always provide getters and setters that convert to primitive types
* if you're not using primitive types directly.
* These primitive types are strings, numbers, characters, booleans and lists of these things.
* *
* @param path The path in config to use * @param path The path in config to use
* @param def The value to use by default * @param def The value to use by default
* @param <T> The type of this variable (only use primitives or String) * @param getter A function that converts a primitive representation to the correct value
* @return The data object that can be used to get or set the value * @param setter A function that converts a value to a primitive representation
</T> */ * @param primDef Whether the default value is a primitive value that needs to be converted to the correct type using the getter
fun <T> getData(path: String, def: T): ConfigData<T> {
var data = datamap[path]
if (data == null) datamap[path] = ConfigData(this, path, def, def, null, null).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/**
* This method overload may be used with any class.
*
* @param path The path in config to use
* @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class) * @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value * @return The data object that can be used to get or set the value
</T> */ </T> */
fun <T> getData(path: String, def: T, getter: Function<Any?, T>?, setter: Function<T, Any?>): ConfigData<T> { @Suppress("UNCHECKED_CAST")
var data = datamap[path] fun <T> getData(
if (data == null) datamap[path] =
ConfigData(this, path, def, setter.apply(def), getter, setter).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/**
* This method overload may be used with any class. The given default value will be run through the getter.
*
* @param path The path in config to use
* @param primitiveDef The **primitive** value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getDataPrimDef(
path: String, path: String,
primitiveDef: Any?, def: T,
getter: Function<Any?, T>, getter: Function<Any?, T>? = null,
setter: Function<T, Any?>? setter: Function<T, Any?>? = null,
readOnly: Boolean = false
): ConfigData<T> { ): ConfigData<T> {
var data = datamap[path] return getData(path, getter ?: Function { it as T }, setter ?: Function { it }, def)
if (data == null) datamap[path] =
ConfigData(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter).also { data = it }
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
} }
/** /**
* This method overload may be used with any class. The given default value will be run through the getter. * You may use this method with any data type and provide a primitive default value.
* These primitive types are strings, numbers, characters, booleans and lists of these things.
* *
* @param path The path in config to use * @param path The path in config to use
* @param primitiveDef The **primitive** value to use by default * @param primitiveDef The **primitive** value to use by default
@ -108,17 +91,17 @@ class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after
* @param <T> The type of this variable (can be any class) * @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value * @return The data object that can be used to get or set the value
</T> */ </T> */
fun <T> getReadOnlyDataPrimDef( @Suppress("UNCHECKED_CAST")
fun <T> getData(
path: String, path: String,
primitiveDef: Any?,
getter: Function<Any?, T>, getter: Function<Any?, T>,
setter: Function<T, Any?>? setter: Function<T, Any?>,
): ReadOnlyConfigData<T> { primitiveDef: Any?,
var data = datamap[path] readOnly: Boolean = false
if (data == null) datamap[path] = ): ConfigData<T> {
ReadOnlyConfigData(this, path, getter.apply(primitiveDef), primitiveDef, getter, setter).also { data = it } val data =
@Suppress("UNCHECKED_CAST") datamap[path] ?: ConfigData(this, path, primitiveDef, getter, setter, readOnly).also { datamap[path] = it }
return data as ReadOnlyConfigData<T> return data as ConfigData<T>
} }
/** /**
@ -139,31 +122,6 @@ class IHaveConfig(var saveAction: Runnable?) { // TODO: Make non-nullable after
return data as ConfigData<T> return data as ConfigData<T>
} }
/**
* This method overload may be used with any class.
*
* @param path The path in config to use
* @param def The value to use by default
* @param getter A function that converts a primitive representation to the correct value
* @param setter A function that converts a value to a primitive representation
* @param <T> The type of this variable (can be any class)
* @return The data object that can be used to get or set the value
</T> */
fun <T> getData(
path: String,
def: Supplier<T>,
getter: Function<Any?, T>?,
setter: Function<T, Any?>
): ConfigData<T> {
var data = datamap[path]
if (data == null) {
val defval = def.get()
datamap[path] = ConfigData(this, path, defval, setter.apply(defval), getter, setter).also { data = it }
}
@Suppress("UNCHECKED_CAST")
return data as ConfigData<T>
}
/** /**
* This method overload should only be used with primitves or String. * This method overload should only be used with primitves or String.
* *

View file

@ -1,124 +1,172 @@
package buttondevteam.lib.architecture; package buttondevteam.lib.architecture
import lombok.val; import buttondevteam.lib.architecture.config.IConfigData
import org.jetbrains.annotations.NotNull; import java.util.function.Function
import java.util.function.Predicate
import java.util.function.UnaryOperator
import kotlin.collections.List as KList
import java.util.ArrayList; class ListConfigData<T> internal constructor(
import java.util.Collection; config: IHaveConfig?,
import java.util.Comparator; path: String,
import java.util.function.Predicate; primitiveDef: kotlin.collections.List<*>,
import java.util.function.UnaryOperator; private val elementGetter: Function<Any?, T>,
private val elementSetter: Function<T, Any?>,
readOnly: Boolean
) : IConfigData<ListConfigData<T>.List> {
val listConfig: ConfigData<List> =
ConfigData(config, path, primitiveDef, { List((it as KList<*>).toMutableList()) }, { it }, readOnly)
public class ListConfigData<T> extends ConfigData<ListConfigData.List<T>> { override val path: String get() = listConfig.path
@SuppressWarnings("unchecked")
ListConfigData(IHaveConfig config, String path, List<T> def) {
super(config, path, def, new ArrayList<>(def), list -> {
var l = new List<>((ArrayList<T>) list);
l.listConfig = def.listConfig;
return l;
}, ArrayList::new);
def.listConfig = this; //Can't make the List class non-static or pass this in the super() constructor
}
public static class List<T> extends ArrayList<T> { override fun reset() {
private ListConfigData<T> listConfig; listConfig.reset()
}
public List(@NotNull Collection<? extends T> c) { override fun get(): List? {
super(c); return listConfig.get()
} }
public List() { override fun set(value: List?) {
} listConfig.set(value)
}
private void update() { inner class List(backingList: MutableList<Any?>) : MutableList<T> {
listConfig.set(this); //Update the config model and start save task if needed private val primitiveList = backingList
} override val size: Int get() = primitiveList.size
private fun update() {
val config = listConfig.config
if (config != null) {
ConfigData.signalChange(config) //Update the config model and start save task if needed
}
}
@Override override fun set(index: Int, element: T): T {
public T set(int index, T element) { val ret = primitiveList.set(index, elementSetter.apply(element))
T ret = super.set(index, element); update()
update(); return elementGetter.apply(ret)
return ret; }
}
@Override override fun add(element: T): Boolean {
public boolean add(T t) { val ret = primitiveList.add(elementSetter.apply(element))
val ret = super.add(t); update()
update(); return ret
return ret; }
}
@Override override fun add(index: Int, element: T) {
public void add(int index, T element) { primitiveList.add(index, elementSetter.apply(element))
super.add(index, element); update()
update(); }
}
@Override override fun removeAt(index: Int): T {
public T remove(int index) { val ret = primitiveList.removeAt(index)
T ret = super.remove(index); update()
update(); return elementGetter.apply(ret)
return ret; }
}
@Override override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> =
public boolean remove(Object o) { List(primitiveList.subList(fromIndex, toIndex))
val ret = super.remove(o);
update();
return ret;
}
@Override override fun remove(element: T): Boolean {
public boolean addAll(Collection<? extends T> c) { val ret = primitiveList.remove(elementSetter.apply(element))
val ret = super.addAll(c); update()
update(); return ret
return ret; }
}
@Override override fun addAll(elements: Collection<T>): Boolean {
public boolean addAll(int index, Collection<? extends T> c) { val ret = primitiveList.addAll(elements.map { elementSetter.apply(it) })
val ret = super.addAll(index, c); update()
update(); return ret
return ret; }
}
@Override override fun addAll(index: Int, elements: Collection<T>): Boolean {
protected void removeRange(int fromIndex, int toIndex) { val ret = primitiveList.addAll(index, elements.map { elementSetter.apply(it) })
super.removeRange(fromIndex, toIndex); update()
update(); return ret
} }
@Override override fun removeAll(elements: Collection<T>): Boolean {
public boolean removeAll(Collection<?> c) { val ret = primitiveList.removeAll(elements.map { elementSetter.apply(it) })
val ret = super.removeAll(c); update()
update(); return ret
return ret; }
}
@Override override fun retainAll(elements: Collection<T>): Boolean {
public boolean retainAll(Collection<?> c) { val ret = primitiveList.retainAll(elements.map { elementSetter.apply(it) })
val ret = super.retainAll(c); update()
update(); return ret
return ret; }
}
@Override override fun removeIf(filter: Predicate<in T>): Boolean {
public boolean removeIf(Predicate<? super T> filter) { val ret = primitiveList.removeIf { filter.test(elementGetter.apply(it)) }
val ret = super.removeIf(filter); update()
update(); return ret
return ret; }
}
@Override override fun replaceAll(operator: UnaryOperator<T>) {
public void replaceAll(UnaryOperator<T> operator) { primitiveList.replaceAll { elementSetter.apply(operator.apply(elementGetter.apply(it))) }
super.replaceAll(operator); update()
update(); }
}
@Override override fun sort(c: Comparator<in T>) {
public void sort(Comparator<? super T> c) { primitiveList.sortWith { o1, o2 -> c.compare(elementGetter.apply(o1), elementGetter.apply(o2)) }
super.sort(c); update()
update(); }
}
} override fun clear() {
primitiveList.clear()
update()
}
override fun get(index: Int): T = elementGetter.apply(primitiveList[index])
override fun isEmpty(): Boolean = primitiveList.isEmpty()
override fun lastIndexOf(element: T): Int = primitiveList.lastIndexOf(elementSetter.apply(element))
override fun indexOf(element: T): Int = primitiveList.indexOf(elementSetter.apply(element))
override fun containsAll(elements: Collection<T>): Boolean =
primitiveList.containsAll(elements.map { elementSetter.apply(it) })
override fun contains(element: T): Boolean = primitiveList.contains(elementSetter.apply(element))
override fun iterator(): MutableIterator<T> {
return object : MutableIterator<T> {
private val iterator = primitiveList.iterator()
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): T = elementGetter.apply(iterator.next())
override fun remove() {
iterator.remove()
update()
}
}
}
override fun listIterator(): MutableListIterator<T> {
return listIterator(0)
}
override fun listIterator(index: Int): MutableListIterator<T> {
return object : MutableListIterator<T> {
private val iterator = primitiveList.listIterator(index)
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): T = elementGetter.apply(iterator.next())
override fun remove() {
iterator.remove()
update()
}
override fun hasPrevious(): Boolean = iterator.hasPrevious()
override fun nextIndex(): Int = iterator.nextIndex()
override fun previous(): T = elementGetter.apply(iterator.previous())
override fun previousIndex(): Int = iterator.previousIndex()
override fun add(element: T) {
iterator.add(elementSetter.apply(element))
update()
}
override fun set(element: T) {
iterator.set(elementSetter.apply(element))
update()
}
}
}
}
} }

View file

@ -1,12 +0,0 @@
package buttondevteam.lib.architecture
import java.util.function.Function
class ReadOnlyConfigData<T> internal constructor(
config: IHaveConfig?,
path: String,
def: T?,
primitiveDef: Any?,
getter: Function<Any?, T>,
setter: Function<T, Any?>
) : ConfigData<T>(config, path, def, primitiveDef, getter, setter)

View file

@ -0,0 +1,9 @@
package buttondevteam.lib.architecture.config
interface IConfigData<T> {
fun reset()
fun get(): T?
fun set(value: T?)
val path: String
}