Более вменяемый BuilderAdapter

This commit is contained in:
DBotThePony 2022-12-31 13:11:09 +07:00
parent b3636e5a55
commit bfae6877c9
Signed by: DBot
GPG Key ID: DCC23B5715498507
4 changed files with 192 additions and 112 deletions

View File

@ -10,7 +10,9 @@ import ru.dbotthepony.kstarbound.defs.*
import ru.dbotthepony.kstarbound.io.json.ConfigurableTypeAdapter
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.CustomEnumTypeAdapter
import ru.dbotthepony.kstarbound.util.NotNullVar
import ru.dbotthepony.kvector.vector.Color
import java.util.concurrent.ConcurrentHashMap
import kotlin.properties.Delegates
class ConfigurableProjectile : RawPrototype<ConfigurableProjectile, ConfiguredProjectile>() {
@ -182,9 +184,9 @@ class ActionConfig : IConfigurableAction {
}
companion object {
val ADAPTER = BuilderAdapter(::ActionConfig, ActionConfig::file).ignoreProperty("action")
val ADAPTER = BuilderAdapter.Builder(::ActionConfig, ActionConfig::file).ignoreKey("action").build()
private val cache = HashMap<String, CActionConfig>()
private val cache = ConcurrentHashMap<String, CActionConfig>()
}
}
@ -192,7 +194,7 @@ class ActionConfig : IConfigurableAction {
* Создает новый прожектайл с заданными параметрами
*/
class ActionProjectile : IConfigurableAction {
lateinit var type: String
var type by NotNullVar<String>()
var angle = 0.0
var inheritDamageFactor = 1.0
@ -201,11 +203,11 @@ class ActionProjectile : IConfigurableAction {
}
companion object {
val ADAPTER = BuilderAdapter(::ActionProjectile,
val ADAPTER = BuilderAdapter.Builder(::ActionProjectile,
ActionProjectile::type,
ActionProjectile::angle,
ActionProjectile::inheritDamageFactor,
).ignoreProperty("action").missingPropertiesAreFatal(false)
).ignoreKey("action").build()
}
}
@ -220,9 +222,9 @@ class ActionSound : IConfigurableAction {
}
companion object {
val ADAPTER = BuilderAdapter(::ActionSound,
val ADAPTER = BuilderAdapter.Builder(::ActionSound,
ActionSound::options,
).ignoreProperty("action")
).ignoreKey("action").build()
}
}
@ -238,10 +240,10 @@ class ActionLoop : IConfigurableAction {
}
companion object {
val ADAPTER = BuilderAdapter(::ActionLoop,
val ADAPTER = BuilderAdapter.Builder(::ActionLoop,
ActionLoop::count,
ActionLoop::body,
).ignoreProperty("action")
).ignoreKey("action").build()
}
}
@ -256,8 +258,8 @@ class ActionActions : IConfigurableAction {
}
companion object {
val ADAPTER = BuilderAdapter(::ActionActions,
val ADAPTER = BuilderAdapter.Builder(::ActionActions,
ActionActions::list,
).ignoreProperty("action")
).ignoreKey("action").build()
}
}

View File

@ -1,140 +1,93 @@
package ru.dbotthepony.kstarbound.io.json
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.Level
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.isSuperclassOf
import kotlin.reflect.full.isSubclassOf
@Suppress("FunctionName")
fun <T : Any> BuilderAdapter(factory: () -> T, vararg fields: KMutableProperty1<T, *>): BuilderAdapter<T> {
val builder = BuilderAdapter.Builder(factory)
for (field in fields) {
builder.auto(field)
}
return builder.build()
}
/**
* Kotlin property aware adapter.
*
* Создаёт пустые классы, а после наполняет их данными, что подходит для builder'ов с очень
* большим количеством возможных данных внутри.
*
* Подходит для игровых структур которые могут быть "разобраны" и пересобраны.
* [TypeAdapter] для классов, которые создаются "без всего", а после наполняются данными (паттерн builder).
*/
class BuilderAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapter<T>() {
private val mappedFields = Object2ObjectArrayMap<String, KMutableProperty1<T, in Any?>>()
// потому что returnType медленный
private val mappedFieldsReturnTypes = Object2ObjectArrayMap<String, KType>()
private val loggedMisses = ObjectArraySet<String>()
class BuilderAdapter<T : Any> private constructor(
/**
* Завод по созданию объектов типа [T]
*/
val factory: () -> T,
private val ignoreProperties = ObjectArraySet<String>()
/**
* Свойства объекта [T], которые можно выставлять
*/
val properties: ImmutableMap<String, WrappedProperty<T, Any?>>,
init {
for (field in fields) {
// потому что в котлине нет понятия KProperty который не имеет getter'а, только setter
require(mappedFields.put(field.name, field as KMutableProperty1<T, in Any?>) == null) { "${field.name} is defined twice" }
mappedFieldsReturnTypes[field.name] = field.returnType
}
}
val fields: List<KMutableProperty1<T, in Any?>> by lazy {
return@lazy ImmutableList.builder<KMutableProperty1<T, in Any?>>().let {
for (v in mappedFields.values.iterator()) {
it.add(v)
}
it.build()
}
}
fun ignoreProperty(vararg value: String): BuilderAdapter<T> {
ignoreProperties.addAll(value)
return this
}
var missingPropertiesAreFatal = true
var missingLogLevel = Level.ERROR
fun missingPropertiesAreFatal(flag: Boolean): BuilderAdapter<T> {
missingPropertiesAreFatal = flag
return this
}
fun missingLogLevel(level: Level): BuilderAdapter<T> {
missingLogLevel = level
return this
}
/**
* Ключи, которые необходимо игнорировать при чтении JSON
*/
val ignoreKeys: ImmutableSet<String>,
) : TypeAdapter<T>() {
private val loggedMisses = ObjectOpenHashSet<String>()
override fun write(writer: JsonWriter, value: T) {
TODO("Not yet implemented")
}
override fun read(reader: JsonReader): T? {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return null
}
override fun read(reader: JsonReader): T {
val missing = ObjectArraySet<WrappedProperty<T, Any?>>()
missing.addAll(properties.values)
reader.beginObject()
val instance = factory.invoke()!!
val instance = factory.invoke()
while (reader.hasNext()) {
val name = reader.nextName()
val field = mappedFields[name]
if (field != null) {
if (ignoreKeys.contains(name)) {
reader.skipValue()
continue
}
val property = properties[name]
if (property != null) {
try {
val peek = reader.peek()
val expectedType = mappedFieldsReturnTypes[name]!!
if (!expectedType.isMarkedNullable && peek == JsonToken.NULL) {
throw NullPointerException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls")
if (!property.returnType.isMarkedNullable && peek == JsonToken.NULL) {
throw NullPointerException("Property ${property.property.name} of ${instance::class.qualifiedName} does not accept nulls")
} else if (peek == JsonToken.NULL) {
field.set(instance, null)
property.property.set(instance, null)
reader.nextNull()
} else {
val classifier = expectedType.classifier
if (classifier is KClass<*>) {
if (classifier.isSuperclassOf(Float::class)) {
val read = reader.nextDouble()
field.set(instance, read.toFloat())
} else if (classifier.isSuperclassOf(Double::class)) {
val read = reader.nextDouble()
field.set(instance, read)
} else if (classifier.isSuperclassOf(Int::class)) {
val read = reader.nextInt()
field.set(instance, read)
} else if (classifier.isSuperclassOf(Long::class)) {
val read = reader.nextLong()
field.set(instance, read)
} else if (classifier.isSuperclassOf(String::class)) {
val read = reader.nextString()
field.set(instance, read)
} else if (classifier.isSuperclassOf(Boolean::class)) {
val read = reader.nextBoolean()
field.set(instance, read)
} else {
field.set(instance, Starbound.gson.fromJson(reader, classifier.java))
}
} else {
throw IllegalStateException("Expected ${field.name} classifier to be KClass, got $classifier")
}
val readValue = property.adapter.read(reader)
property.property.set(instance, readValue)
check(missing.remove(property))
}
} catch(err: Throwable) {
throw JsonSyntaxException(
"Reading property ${field.name} of ${instance::class.qualifiedName} near ${reader.path}",
err
)
throw JsonSyntaxException("Reading property ${property.property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
}
} else if (!ignoreProperties.contains(name) && missingPropertiesAreFatal) {
throw JsonSyntaxException("Property $name is not present in ${instance::class.qualifiedName}")
} else {
if (!ignoreProperties.contains(name) && !loggedMisses.contains(name)) {
LOGGER.log(missingLogLevel, "{} has no property for storing {}", instance::class.qualifiedName, name)
if (!loggedMisses.contains(name)) {
LOGGER.warn("{} has no property for storing {}", instance::class.qualifiedName, name)
loggedMisses.add(name)
}
@ -143,9 +96,99 @@ class BuilderAdapter<T>(val factory: () -> T, vararg fields: KMutableProperty1<T
}
reader.endObject()
for (property in missing) {
if (property.mustBePresent == null) {
// null - проверяем, есть ли делегат
}
}
return instance
}
data class WrappedProperty<T, V : Any?>(
val property: KMutableProperty1<T, V>,
val adapter: TypeAdapter<V>,
val mustBePresent: Boolean?,
) {
// кеш
val returnType = property.returnType
}
class PropertyConfigurator<T, V : Any?>(
val property: KMutableProperty1<T, V>,
val adapter: TypeAdapter<V>,
) {
/**
* Обязана ли присутствовать эта переменная внутри JSON структуры.
*/
var mustBePresent: Boolean? = null
}
class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) {
private val properties = ArrayList<WrappedProperty<T, *>>()
private val ignoreKeys = ObjectArraySet<String>()
init {
for (field in fields)
auto(field)
}
fun <V> add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
if (properties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice")
}
ignoreKeys.remove(property.name)
val config = PropertyConfigurator(property, adapter)
configurator.invoke(config)
properties.add(WrappedProperty(
property,
adapter,
mustBePresent = config.mustBePresent
))
return this
}
fun <V> auto(property: KMutableProperty1<T, V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val returnType = property.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${property.name}!")
if (classifier.isSubclassOf(List::class)) {
throw IllegalArgumentException("${property.name} is a List, please use autoList() method instead")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${property.name} is a Map, please use autoMap() method instead")
}
@Suppress("unchecked_cast")
return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator)
}
fun ignoreKey(name: String): Builder<T> {
if (properties.any { it.property.name == name }) {
throw IllegalArgumentException("Can not ignore key $name because we have property with this name!")
}
ignoreKeys.add(name)
return this
}
@Suppress("unchecked_cast")
fun build(): BuilderAdapter<T> {
val map = ImmutableMap.Builder<String, WrappedProperty<T, Any?>>()
for (property in properties)
map.put(property.property.name, property as WrappedProperty<T, Any?>)
return BuilderAdapter(factory, map.build(), ImmutableSet.copyOf(ignoreKeys))
}
}
companion object {
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java)
}

View File

@ -36,7 +36,7 @@ private data class PackedProperty<Clazz : Any, T>(
}
/**
* TypeAdapter для классов которые создаются единожды и более не меняются ("бетонных классов").
* [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе.
*/
class FactoryAdapter<T : Any> private constructor(
val bound: KClass<T>,
@ -400,6 +400,15 @@ class FactoryAdapter<T : Any> private constructor(
fun <In> auto(field: KProperty1<T, In>, transformer: (In) -> In = { it }): Builder<T> {
val returnType = field.returnType
val classifier = returnType.classifier as? KClass<*> ?: throw ClassCastException("Unable to cast ${returnType.classifier} to KClass of property ${field.name}!")
if (classifier.isSubclassOf(List::class)) {
throw IllegalArgumentException("${field.name} is a List, please use autoList() method instead")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${field.name} is a Map, please use autoMap() method instead")
}
types.add(PackedProperty(field, LazyTypeProvider(classifier.java) as TypeAdapter<Any?>, transformer = transformer as (Any?) -> Any?))
return this
}

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.util
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.properties.Delegates
/**
* Аналог [Delegates.notNull], но со свойством [isInitialized]
*/
class NotNullVar<V : Any> : ReadWriteProperty<Any, V> {
private var value: V? = null
/**
* Имеет ли данный делегат не-null значение
*/
val isInitialized: Boolean
get() = value != null
override fun getValue(thisRef: Any, property: KProperty<*>): V {
return value ?: throw IllegalStateException("Property ${property.name} was not initialized")
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: V) {
this.value = value
}
}