ThingDescription, flat json свойства и делегирование

This commit is contained in:
DBotThePony 2023-01-20 22:08:59 +07:00
parent 4675197a04
commit b077b22180
Signed by: DBot
GPG Key ID: DCC23B5715498507
14 changed files with 281 additions and 61 deletions

View File

@ -122,7 +122,7 @@ object Starbound {
val FUNCTION: Map<String, JsonFunction> = Collections.unmodifiableMap(functions) val FUNCTION: Map<String, JsonFunction> = Collections.unmodifiableMap(functions)
val ITEM: Map<String, IItemDefinition> = Collections.unmodifiableMap(items) val ITEM: Map<String, IItemDefinition> = Collections.unmodifiableMap(items)
val STRING_INTERNER: Interner<String> = Interners.newStrongInterner() val STRING_INTERNER: Interner<String> = Interners.newWeakInterner()
val STRING_ADAPTER: TypeAdapter<String> = object : TypeAdapter<String>() { val STRING_ADAPTER: TypeAdapter<String> = object : TypeAdapter<String>() {
override fun write(out: JsonWriter, value: String) { override fun write(out: JsonWriter, value: String) {
@ -171,6 +171,7 @@ object Starbound {
.registerTypeAdapter(LeveledStatusEffect.ADAPTER) .registerTypeAdapter(LeveledStatusEffect.ADAPTER)
.registerTypeAdapter(MaterialReference.Companion) .registerTypeAdapter(MaterialReference.Companion)
.registerTypeAdapter(ThingDescription.ADAPTER)
.registerTypeAdapter(ItemPrototype.ADAPTER) .registerTypeAdapter(ItemPrototype.ADAPTER)
.registerTypeAdapter(CurrencyItemPrototype.ADAPTER) .registerTypeAdapter(CurrencyItemPrototype.ADAPTER)

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import ru.dbotthepony.kstarbound.io.json.FactoryAdapter
interface IThingWithDescription { interface IThingWithDescription {
/** /**
* Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание", * Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание",
@ -21,4 +23,35 @@ interface IThingWithDescription {
* * The Poptop hums beautifully to confuse its prey. * * The Poptop hums beautifully to confuse its prey.
*/ */
val description: String val description: String
/**
* Полное описание штуки для определённых рас
*
* Пример для Microwave Oven:
* Apex: A type of oven.
* Avian: A bizarre cooking device.
* Floran: Floran likess raw meat, sssometimes cooked meat is good too.
* Glitch: Irked. It is encrusted with spattered food, who left it in this state?
* Human: A microwave. Gotta get me some jacket potatoes.
* Hylotl: A strange, spinning oven.
* Novakid: This lil' rotatin' oven cooks food at speed!
*/
val racialDescription: Map<String, String> get() = mapOf()
/**
* Кратное описание штуки для определённых рас
*/
val racialShortDescription: Map<String, String> get() = mapOf()
}
data class ThingDescription(
override val shortdescription: String,
override val description: String
) : IThingWithDescription {
companion object {
val ADAPTER = FactoryAdapter.Builder(ThingDescription::class,
ThingDescription::shortdescription,
ThingDescription::description)
.build()
}
} }

View File

@ -1,8 +1,9 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription
data class ArmorItemDefinition( data class ArmorItemDefinition(
override val shortdescription: String,
override val description: String,
override val itemName: String, override val itemName: String,
override val price: Long, override val price: Long,
override val rarity: ItemRarity, override val rarity: ItemRarity,
@ -29,5 +30,7 @@ data class ArmorItemDefinition(
override val armorType: ArmorPieceType, override val armorType: ArmorPieceType,
val descriptionData: ThingDescription,
val json: Map<String, Any>, val json: Map<String, Any>,
) : IArmorItemDefinition ) : IArmorItemDefinition, IThingWithDescription by descriptionData

View File

@ -26,8 +26,7 @@ class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition {
override fun assemble(): IItemDefinition { override fun assemble(): IItemDefinition {
return ArmorItemDefinition( return ArmorItemDefinition(
shortdescription = shortdescription, descriptionData = descriptionData,
description = description,
itemName = itemName, itemName = itemName,
price = price, price = price,
rarity = rarity, rarity = rarity,

View File

@ -1,8 +1,9 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription
data class CurrencyItemDefinition( data class CurrencyItemDefinition(
override val shortdescription: String,
override val description: String,
override val itemName: String, override val itemName: String,
override val price: Long, override val price: Long,
override val rarity: ItemRarity, override val rarity: ItemRarity,
@ -27,5 +28,7 @@ data class CurrencyItemDefinition(
override val currency: String, override val currency: String,
override val value: Long, override val value: Long,
val descriptionData: ThingDescription,
val json: Map<String, Any>, val json: Map<String, Any>,
) : ICurrencyItemDefinition ) : ICurrencyItemDefinition, IThingWithDescription by descriptionData

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.enrollMap import ru.dbotthepony.kstarbound.defs.enrollMap
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.util.NotNullVar import ru.dbotthepony.kstarbound.util.NotNullVar
@ -19,8 +20,7 @@ class CurrencyItemPrototype : ItemPrototype(), ICurrencyItemDefinition {
override fun assemble(): IItemDefinition { override fun assemble(): IItemDefinition {
return CurrencyItemDefinition( return CurrencyItemDefinition(
shortdescription = shortdescription, descriptionData = descriptionData,
description = description,
itemName = itemName, itemName = itemName,
price = price, price = price,
rarity = rarity, rarity = rarity,

View File

@ -1,8 +1,9 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription
data class ItemDefinition( data class ItemDefinition(
override val shortdescription: String,
override val description: String,
override val itemName: String, override val itemName: String,
override val price: Long, override val price: Long,
override val rarity: ItemRarity, override val rarity: ItemRarity,
@ -19,5 +20,7 @@ data class ItemDefinition(
override val radioMessagesOnPickup: List<String>, override val radioMessagesOnPickup: List<String>,
override val fuelAmount: Long?, override val fuelAmount: Long?,
val descriptionData: ThingDescription,
val json: Map<String, Any> val json: Map<String, Any>
) : IItemDefinition ) : IItemDefinition, IThingWithDescription by descriptionData

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.enrollMap import ru.dbotthepony.kstarbound.defs.enrollMap
import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.BuilderAdapter
import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder
@ -24,6 +25,8 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder {
final override var radioMessagesOnPickup: List<String> = listOf() final override var radioMessagesOnPickup: List<String> = listOf()
final override var fuelAmount: Long? = null final override var fuelAmount: Long? = null
var descriptionData: ThingDescription by NotNullVar()
var json: Map<String, Any> = mapOf() var json: Map<String, Any> = mapOf()
final override fun acceptJson(json: MutableMap<String, Any>) { final override fun acceptJson(json: MutableMap<String, Any>) {
@ -32,8 +35,7 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder {
open fun assemble(): IItemDefinition { open fun assemble(): IItemDefinition {
return ItemDefinition( return ItemDefinition(
shortdescription = shortdescription, descriptionData = descriptionData,
description = description,
itemName = itemName, itemName = itemName,
price = price, price = price,
rarity = rarity, rarity = rarity,
@ -78,6 +80,7 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder {
auto(ItemPrototype::twoHanded) auto(ItemPrototype::twoHanded)
autoList(ItemPrototype::radioMessagesOnPickup) autoList(ItemPrototype::radioMessagesOnPickup)
auto(ItemPrototype::fuelAmount) auto(ItemPrototype::fuelAmount)
flat(ItemPrototype::descriptionData)
} }
} }
} }

View File

@ -1,10 +1,10 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.MaterialReference import ru.dbotthepony.kstarbound.defs.MaterialReference
import ru.dbotthepony.kstarbound.defs.ThingDescription
data class LiquidItemDefinition( data class LiquidItemDefinition(
override val shortdescription: String,
override val description: String,
override val itemName: String, override val itemName: String,
override val price: Long, override val price: Long,
override val rarity: ItemRarity, override val rarity: ItemRarity,
@ -23,5 +23,7 @@ data class LiquidItemDefinition(
override val liquid: MaterialReference, override val liquid: MaterialReference,
val descriptionData: ThingDescription,
val json: Map<String, Any> val json: Map<String, Any>
) : ILiquidItem ) : ILiquidItem, IThingWithDescription by descriptionData

View File

@ -20,8 +20,7 @@ class LiquidItemPrototype : ItemPrototype() {
override fun assemble(): IItemDefinition { override fun assemble(): IItemDefinition {
return LiquidItemDefinition( return LiquidItemDefinition(
shortdescription = shortdescription, descriptionData = descriptionData,
description = description,
itemName = itemName, itemName = itemName,
price = price, price = price,
rarity = rarity, rarity = rarity,

View File

@ -1,10 +1,10 @@
package ru.dbotthepony.kstarbound.defs.item package ru.dbotthepony.kstarbound.defs.item
import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.MaterialReference import ru.dbotthepony.kstarbound.defs.MaterialReference
import ru.dbotthepony.kstarbound.defs.ThingDescription
data class MaterialItemDefinition( data class MaterialItemDefinition(
override val shortdescription: String,
override val description: String,
override val itemName: String, override val itemName: String,
override val price: Long, override val price: Long,
override val rarity: ItemRarity, override val rarity: ItemRarity,
@ -23,5 +23,7 @@ data class MaterialItemDefinition(
override val material: MaterialReference, override val material: MaterialReference,
val descriptionData: ThingDescription,
val json: Map<String, Any> val json: Map<String, Any>
) : IMaterialItem ) : IMaterialItem, IThingWithDescription by descriptionData

View File

@ -20,8 +20,7 @@ class MaterialItemPrototype : ItemPrototype() {
override fun assemble(): IItemDefinition { override fun assemble(): IItemDefinition {
return MaterialItemDefinition( return MaterialItemDefinition(
shortdescription = shortdescription, descriptionData = descriptionData,
description = description,
itemName = itemName, itemName = itemName,
price = price, price = price,
rarity = rarity, rarity = rarity,

View File

@ -16,6 +16,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.flattenJsonElement import ru.dbotthepony.kstarbound.defs.flattenJsonElement
import ru.dbotthepony.kstarbound.util.NotNullVar import ru.dbotthepony.kstarbound.util.NotNullVar
import kotlin.properties.Delegates
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
@ -68,6 +69,11 @@ class BuilderAdapter<T : Any> private constructor(
*/ */
val properties: ImmutableMap<String, WrappedProperty<T, *>>, val properties: ImmutableMap<String, WrappedProperty<T, *>>,
/**
* Свойства объекта [T], которые можно выставлять
*/
val flatProperties: ImmutableMap<String, WrappedProperty<T, *>>,
/** /**
* Ключи, которые необходимо игнорировать при чтении JSON * Ключи, которые необходимо игнорировать при чтении JSON
*/ */
@ -77,6 +83,11 @@ class BuilderAdapter<T : Any> private constructor(
* @see Builder.extraPropertiesAreFatal * @see Builder.extraPropertiesAreFatal
*/ */
val extraPropertiesAreFatal: Boolean, val extraPropertiesAreFatal: Boolean,
/**
* @see Builder.logMisses
*/
val logMisses: Boolean,
) : TypeAdapter<T>() { ) : TypeAdapter<T>() {
private val loggedMisses = ObjectOpenHashSet<String>() private val loggedMisses = ObjectOpenHashSet<String>()
@ -92,15 +103,16 @@ class BuilderAdapter<T : Any> private constructor(
missing.addAll(properties.values) missing.addAll(properties.values)
val instance = factory.invoke() val instance = factory.invoke()
var json: JsonObject by Delegates.notNull()
if (instance is IJsonHolder) { if (instance is IJsonHolder || flatProperties.isNotEmpty()) {
val obj = TypeAdapters.JSON_ELEMENT.read(reader) json = TypeAdapters.JSON_ELEMENT.read(reader) as JsonObject
reader = JsonTreeReader(obj) reader = JsonTreeReader(json)
if (instance is INativeJsonHolder) { if (instance is INativeJsonHolder) {
instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.STRING_INTERNER::intern)) instance.acceptJson(flattenJsonElement(json, Starbound.STRING_INTERNER::intern))
} else { } else if (instance is IJsonHolder) {
instance.acceptJson(obj.asJsonObject) instance.acceptJson(json)
} }
} }
@ -142,7 +154,7 @@ class BuilderAdapter<T : Any> private constructor(
throw JsonSyntaxException("$name is not a valid property of ${instance::class.qualifiedName}") throw JsonSyntaxException("$name is not a valid property of ${instance::class.qualifiedName}")
} }
if (!loggedMisses.contains(name)) { if (logMisses && !loggedMisses.contains(name)) {
LOGGER.warn("${instance::class.qualifiedName} has no property for storing $name") LOGGER.warn("${instance::class.qualifiedName} has no property for storing $name")
loggedMisses.add(name) loggedMisses.add(name)
} }
@ -153,9 +165,25 @@ class BuilderAdapter<T : Any> private constructor(
reader.endObject() reader.endObject()
for (property in flatProperties.values) {
try {
val read = property.adapter.read(JsonTreeReader(json))
if (!property.returnType.isMarkedNullable && read == null) {
throw NullPointerException("Property ${property.name} of ${instance::class.qualifiedName} does not accept nulls (flat property adapter returned NULL)")
} else if (read == null) {
property.set(instance, null)
} else {
property.set(instance, read)
}
} catch(err: Throwable) {
throw JsonSyntaxException("Reading flat property ${property.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
}
}
for (property in missing) { for (property in missing) {
if (property.mustBePresent == true) { if (property.mustBePresent == true) {
throw JsonSyntaxException("${instance::class.qualifiedName} demands for ${property.name} to be present, however, it is missing") throw JsonSyntaxException("${instance::class.qualifiedName} demands for ${property.name} to be present, however, it is missing from JSON structure")
} else if (property.mustBePresent == null) { } else if (property.mustBePresent == null) {
if (property.returnType.isMarkedNullable) { if (property.returnType.isMarkedNullable) {
continue continue
@ -214,8 +242,10 @@ class BuilderAdapter<T : Any> private constructor(
class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) { class Builder<T : Any>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) {
private val properties = ArrayList<WrappedProperty<T, *>>() private val properties = ArrayList<WrappedProperty<T, *>>()
private val flatProperties = ArrayList<WrappedProperty<T, *>>()
private val ignoreKeys = ObjectArraySet<String>() private val ignoreKeys = ObjectArraySet<String>()
var extraPropertiesAreFatal = false var extraPropertiesAreFatal = false
var logMisses = true
/** /**
* Являются ли "лишние" ключи в JSON структуре ошибкой. * Являются ли "лишние" ключи в JSON структуре ошибкой.
@ -224,24 +254,45 @@ class BuilderAdapter<T : Any> private constructor(
* то [extraPropertiesAreFatal] можно скомбинировать с [ignoreKey]. * то [extraPropertiesAreFatal] можно скомбинировать с [ignoreKey].
*/ */
fun extraPropertiesAreFatal(flag: Boolean = true): Builder<T> { fun extraPropertiesAreFatal(flag: Boolean = true): Builder<T> {
check(flatProperties.isEmpty() || !flag) { "Can't have both flattened properties and extraPropertiesAreFatal" }
extraPropertiesAreFatal = flag extraPropertiesAreFatal = flag
return this return this
} }
/**
* Логировать ли несуществующие свойства у класса когда они попадаются в исходной JSON структуре
*/
fun logMisses(flag: Boolean = true): Builder<T> {
logMisses = flag
return this
}
init { init {
for (field in fields) for (field in fields)
auto(field) auto(field)
} }
fun <V> add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> { private fun <V> _add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit): PropertyConfigurator<T, *> {
if (properties.any { it.property == property }) { if (properties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice") throw IllegalArgumentException("Property $property is defined twice")
} }
if (flatProperties.any { it.property == property }) {
throw IllegalArgumentException("Property $property is defined twice")
}
ignoreKeys.remove(property.name) ignoreKeys.remove(property.name)
val config = PropertyConfigurator(property, adapter) val config = PropertyConfigurator(property, adapter)
configurator.invoke(config) configurator.invoke(config)
return config
}
/**
* Добавляет указанное свойство в будущий адаптер с указанным [adapter]
*/
fun <V> add(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val config = _add(property, adapter, configurator)
properties.add(WrappedProperty( properties.add(WrappedProperty(
property, property,
@ -252,6 +303,38 @@ class BuilderAdapter<T : Any> private constructor(
return this return this
} }
/**
* Добавляет указанное свойство в будущий адаптер, как плоский объект внутри данного на том же уровне, что и сам объект, используя адаптер [adapter]
*
* Пример:
* ```json
* {
* "prop_belong_to_a_1": ...,
* "prop_belong_to_a_2": ...,
* "prop_belong_to_b_1": ...,
* }
* ```
*
* В данном случае, можно указать `b` как плоский класс внутри `a`.
*
* Данный подход позволяет избавиться от постоянного наследования и реализации одного и того же интерфейса во множестве других классов.
*
* Флаг [extraPropertiesAreFatal] не поддерживается с данными свойствами
*/
fun <V> flat(property: KMutableProperty1<T, V>, adapter: TypeAdapter<V>, configurator: PropertyConfigurator<T, V>.() -> Unit = {}): Builder<T> {
val config = _add(property, adapter, configurator)
check(!extraPropertiesAreFatal) { "Can't have both flattened properties and extraPropertiesAreFatal" }
flatProperties.add(WrappedProperty(
property,
adapter,
mustBePresent = config.mustBePresent
))
return this
}
/** /**
* Автоматически определяет тип свойства и необходимый [TypeAdapter] * Автоматически определяет тип свойства и необходимый [TypeAdapter]
*/ */
@ -271,6 +354,25 @@ class BuilderAdapter<T : Any> private constructor(
return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator) return add(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator)
} }
/**
* Автоматически определяет тип свойства и необходимый [TypeAdapter], инкапсулированный в данный типе на одном уровне
*/
fun <V> flat(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() or directly specify type adapter method instead")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${property.name} is a Map, please use autoMap() or directly specify type adapter method instead")
}
@Suppress("unchecked_cast") // classifier.java не имеет обозначенного типа
return flat(property, LazyTypeProvider(classifier.java) as TypeAdapter<V>, configurator)
}
/** /**
* Автоматически создаёт [ListAdapter] для заданного свойства * Автоматически создаёт [ListAdapter] для заданного свойства
*/ */
@ -300,11 +402,18 @@ class BuilderAdapter<T : Any> private constructor(
for (property in properties) for (property in properties)
map.put(property.property.name, property) map.put(property.property.name, property)
val map2 = ImmutableMap.Builder<String, WrappedProperty<T, *>>()
for (property in flatProperties)
map2.put(property.property.name, property)
return BuilderAdapter( return BuilderAdapter(
factory = factory, factory = factory,
properties = map.build(), properties = map.build(),
flatProperties = map2.build(),
ignoreKeys = ImmutableSet.copyOf(ignoreKeys), ignoreKeys = ImmutableSet.copyOf(ignoreKeys),
extraPropertiesAreFatal = extraPropertiesAreFatal, extraPropertiesAreFatal = extraPropertiesAreFatal,
logMisses = logMisses,
) )
} }
} }

View File

@ -23,18 +23,11 @@ import ru.dbotthepony.kstarbound.getValue
import ru.dbotthepony.kstarbound.setValue import ru.dbotthepony.kstarbound.setValue
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import kotlin.jvm.internal.DefaultConstructorMarker import kotlin.jvm.internal.DefaultConstructorMarker
import kotlin.properties.Delegates
import kotlin.reflect.* import kotlin.reflect.*
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.isSupertypeOf import kotlin.reflect.full.isSupertypeOf
private data class PackedProperty<Clazz : Any, T>(
val property: KProperty1<Clazz, T>,
val adapter: TypeAdapter<T>,
val transformer: (T) -> T = { it }
) {
val returnType = property.returnType
}
/** /**
* [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе. * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе.
*/ */
@ -44,11 +37,18 @@ class FactoryAdapter<T : Any> private constructor(
val asJsonArray: Boolean, val asJsonArray: Boolean,
val storesJson: Boolean val storesJson: Boolean
) : TypeAdapter<T>() { ) : TypeAdapter<T>() {
private data class PackedProperty<Clazz : Any, T>(
val property: KProperty1<Clazz, T>,
val adapter: TypeAdapter<T>,
val transformer: (T) -> T = { it },
val isFlat: Boolean
) {
val returnType = property.returnType
}
private val name2index = Object2IntArrayMap<String>() private val name2index = Object2IntArrayMap<String>()
private val loggedMisses = ObjectArraySet<String>() private val loggedMisses = ObjectArraySet<String>()
var currentSymbolicName by ThreadLocal<String>()
init { init {
name2index.defaultReturnValue(-1) name2index.defaultReturnValue(-1)
@ -218,20 +218,24 @@ class FactoryAdapter<T : Any> private constructor(
readValues[fieldId] = (tuple.transformer as (Any?) -> Any?)(adapter.read(reader)) readValues[fieldId] = (tuple.transformer as (Any?) -> Any?)(adapter.read(reader))
presentValues[fieldId] = true presentValues[fieldId] = true
} catch(err: Throwable) { } catch(err: Throwable) {
throw JsonSyntaxException("Exception reading field ${field.name} for ${bound.qualifiedName}", err) throw JsonSyntaxException("Reading field ${field.name} for ${bound.qualifiedName}", err)
} }
fieldId++ fieldId++
} }
// иначе - читаем как json object // иначе - читаем как json object
} else { } else {
if (storesJson) { var json: JsonObject by Delegates.notNull()
val hasFlatValues = types.any { it.isFlat }
if (storesJson || hasFlatValues) {
val readMap = TypeAdapters.JSON_ELEMENT.read(reader) val readMap = TypeAdapters.JSON_ELEMENT.read(reader)
if (readMap !is JsonObject) { if (readMap !is JsonObject) {
throw JsonParseException("Expected JSON element to be a Map, ${readMap::class.qualifiedName} given") throw JsonParseException("Expected JSON element to be a Map, ${readMap::class.qualifiedName} given")
} }
json = readMap
reader = JsonTreeReader(readMap) reader = JsonTreeReader(readMap)
readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map<String, Any>, Starbound.STRING_INTERNER::intern) readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map<String, Any>, Starbound.STRING_INTERNER::intern)
} }
@ -243,7 +247,7 @@ class FactoryAdapter<T : Any> private constructor(
val fieldId = name2index.getInt(name) val fieldId = name2index.getInt(name)
if (fieldId == -1) { if (fieldId == -1) {
if (!storesJson && loggedMisses.add(name)) { if (!storesJson && !hasFlatValues && loggedMisses.add(name)) {
if (currentSymbolicName == null) { if (currentSymbolicName == null) {
LOGGER.warn("${bound.qualifiedName} has no property for storing $name ") LOGGER.warn("${bound.qualifiedName} has no property for storing $name ")
} else { } else {
@ -254,17 +258,34 @@ class FactoryAdapter<T : Any> private constructor(
reader.skipValue() reader.skipValue()
} else { } else {
val tuple = types[fieldId] val tuple = types[fieldId]
if (tuple.isFlat) {
reader.skipValue()
continue
}
val (field, adapter) = tuple val (field, adapter) = tuple
try { try {
readValues[fieldId] = (tuple.transformer as (Any?) -> Any?)(adapter.read(reader)) readValues[fieldId] = (tuple.transformer as (Any?) -> Any?)(adapter.read(reader))
presentValues[fieldId] = true presentValues[fieldId] = true
} catch(err: Throwable) { } catch(err: Throwable) {
if (currentSymbolicName == null) { throw JsonSyntaxException("Reading field ${field.name} for ${bound.qualifiedName}", err)
throw JsonSyntaxException("Exception reading field ${field.name} for ${bound.qualifiedName}", err) }
} else { }
throw JsonSyntaxException("Exception reading field ${field.name} for ${bound.qualifiedName} (reading: $currentSymbolicName)", err) }
for ((i, property) in types.withIndex()) {
if (property.isFlat) {
try {
val read = property.adapter.read(JsonTreeReader(json))
if (read != null) {
presentValues[i] = true
readValues[i] = read
} }
} catch(err: Throwable) {
throw JsonSyntaxException("Reading flat field ${property.property.name} for ${bound.qualifiedName}", err)
} }
} }
} }
@ -381,10 +402,18 @@ class FactoryAdapter<T : Any> private constructor(
} }
/** /**
* Добавляет поле с определённым адаптером * Добавляет свойство с определённым [adapter]
*/ */
fun <V> add(field: KProperty1<T, V>, adapter: TypeAdapter<V>): Builder<T> { fun <V> add(field: KProperty1<T, V>, adapter: TypeAdapter<V>): Builder<T> {
types.add(PackedProperty(field, adapter)) types.add(PackedProperty(field, adapter, isFlat = false))
return this
}
/**
* Добавляет свойство с определённым [adapter], которое находится на том же уровне что и данный объект внутри JSON структуры
*/
fun <V> flat(field: KProperty1<T, V>, adapter: TypeAdapter<V>): Builder<T> {
types.add(PackedProperty(field, adapter, isFlat = true))
return this return this
} }
@ -399,6 +428,17 @@ class FactoryAdapter<T : Any> private constructor(
return this return this
} }
/**
* Добавляет поля без generic типов и без преобразователей
*/
@Suppress("unchecked_cast")
fun autoFlat(vararg fields: KProperty1<T, *>): Builder<T> {
for (field in fields)
autoFlat(field)
return this
}
/** /**
* Автоматически определяет тип поля и необходимый адаптор типа к нему * Автоматически определяет тип поля и необходимый адаптор типа к нему
*/ */
@ -415,7 +455,27 @@ class FactoryAdapter<T : Any> private constructor(
throw IllegalArgumentException("${field.name} is a Map, please use autoMap() method instead") 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?)) types.add(PackedProperty(field, LazyTypeProvider(classifier.java) as TypeAdapter<Any?>, transformer = transformer as (Any?) -> Any?, isFlat = false))
return this
}
/**
* Автоматически определяет тип поля и необходимый адаптор типа к нему
*/
@Suppress("unchecked_cast")
fun <In> autoFlat(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")
}
if (classifier.isSubclassOf(Map::class)) {
throw IllegalArgumentException("${field.name} is a Map")
}
types.add(PackedProperty(field, LazyTypeProvider(classifier.java) as TypeAdapter<Any?>, transformer = transformer as (Any?) -> Any?, isFlat = true))
return this return this
} }
@ -425,7 +485,7 @@ class FactoryAdapter<T : Any> private constructor(
* Список неизменяем (создаётся объект [ImmutableList]) * Список неизменяем (создаётся объект [ImmutableList])
*/ */
fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (V) -> V = { it }): Builder<T> { fun <V : Any> list(field: KProperty1<T, List<V>?>, type: Class<V>, transformer: (V) -> V = { it }): Builder<T> {
types.add(PackedProperty(field, ListAdapter(type, transformer).nullSafe())) types.add(PackedProperty(field, ListAdapter(type, transformer).nullSafe(), isFlat = false))
return this return this
} }
@ -444,7 +504,7 @@ class FactoryAdapter<T : Any> private constructor(
* Таблица неизменяема (создаётся объект [ImmutableMap]) * Таблица неизменяема (создаётся объект [ImmutableMap])
*/ */
fun <K, V> mapAsArray(field: KProperty1<T, Map<K, V>>, keyType: Class<K>, valueType: Class<V>): Builder<T> { fun <K, V> mapAsArray(field: KProperty1<T, Map<K, V>>, keyType: Class<K>, valueType: Class<V>): Builder<T> {
types.add(PackedProperty(field, MapAdapter(keyType, valueType))) types.add(PackedProperty(field, MapAdapter(keyType, valueType), isFlat = false))
return this return this
} }
@ -468,7 +528,7 @@ class FactoryAdapter<T : Any> private constructor(
* Таблица неизменяема (создаётся объект [ImmutableMap]) * Таблица неизменяема (создаётся объект [ImmutableMap])
*/ */
fun <K : Any, V : Any> mapAsArray(field: KProperty1<T, Map<K, V>?>, keyType: KClass<K>, valueType: KClass<V>): Builder<T> { fun <K : Any, V : Any> mapAsArray(field: KProperty1<T, Map<K, V>?>, keyType: KClass<K>, valueType: KClass<V>): Builder<T> {
types.add(PackedProperty(field, MapAdapter(keyType.java, valueType.java).nullSafe())) types.add(PackedProperty(field, MapAdapter(keyType.java, valueType.java).nullSafe(), isFlat = false))
return this return this
} }
@ -478,7 +538,7 @@ class FactoryAdapter<T : Any> private constructor(
* Таблица неизменяема (создаётся объект [ImmutableMap]) * Таблица неизменяема (создаётся объект [ImmutableMap])
*/ */
fun <V> mapAsObject(field: KProperty1<T, Map<String, V>?>, valueType: Class<V>): Builder<T> { fun <V> mapAsObject(field: KProperty1<T, Map<String, V>?>, valueType: Class<V>): Builder<T> {
types.add(PackedProperty(field, String2ObjectAdapter(valueType).nullSafe())) types.add(PackedProperty(field, String2ObjectAdapter(valueType).nullSafe(), isFlat = false))
return this return this
} }
@ -488,7 +548,7 @@ class FactoryAdapter<T : Any> private constructor(
* Таблица неизменяема (создаётся объект [ImmutableMap]) * Таблица неизменяема (создаётся объект [ImmutableMap])
*/ */
fun <V : Any> mapAsObject(field: KProperty1<T, Map<String, V>?>, valueType: KClass<V>): Builder<T> { fun <V : Any> mapAsObject(field: KProperty1<T, Map<String, V>?>, valueType: KClass<V>): Builder<T> {
types.add(PackedProperty(field, String2ObjectAdapter(valueType.java).nullSafe())) types.add(PackedProperty(field, String2ObjectAdapter(valueType.java).nullSafe(), isFlat = false))
return this return this
} }
@ -505,16 +565,20 @@ class FactoryAdapter<T : Any> private constructor(
} }
fun build(): FactoryAdapter<T> { fun build(): FactoryAdapter<T> {
check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" }
return FactoryAdapter( return FactoryAdapter(
bound = clazz, bound = clazz,
types = ImmutableList.copyOf(types), types = ImmutableList.copyOf(types),
asJsonArray = asList, asJsonArray = asList,
storesJson = storesJson storesJson = storesJson,
) )
} }
} }
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
var currentSymbolicName by ThreadLocal<String>()
} }
} }