From b077b22180be63cdc023fa515908250c4c3ce3b4 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 20 Jan 2023 22:08:59 +0700 Subject: [PATCH] =?UTF-8?q?ThingDescription,=20flat=20json=20=D1=81=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0=20=D0=B8=20=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/dbotthepony/kstarbound/Starbound.kt | 3 +- .../kstarbound/defs/IThingWithDescription.kt | 33 +++++ .../defs/item/ArmorItemDefinition.kt | 9 +- .../defs/item/ArmorItemPrototype.kt | 3 +- .../defs/item/CurrencyItemDefinition.kt | 9 +- .../defs/item/CurrencyItemPrototype.kt | 4 +- .../kstarbound/defs/item/ItemDefinition.kt | 9 +- .../kstarbound/defs/item/ItemPrototype.kt | 7 +- .../defs/item/LiquidItemDefinition.kt | 8 +- .../defs/item/LiquidItemPrototype.kt | 3 +- .../defs/item/MaterialItemDefinition.kt | 8 +- .../defs/item/MaterialItemPrototype.kt | 3 +- .../kstarbound/io/json/BuilderAdapter.kt | 127 ++++++++++++++++-- .../kstarbound/io/json/FactoryAdapter.kt | 116 ++++++++++++---- 14 files changed, 281 insertions(+), 61 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 784e8077..901ee82d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -122,7 +122,7 @@ object Starbound { val FUNCTION: Map = Collections.unmodifiableMap(functions) val ITEM: Map = Collections.unmodifiableMap(items) - val STRING_INTERNER: Interner = Interners.newStrongInterner() + val STRING_INTERNER: Interner = Interners.newWeakInterner() val STRING_ADAPTER: TypeAdapter = object : TypeAdapter() { override fun write(out: JsonWriter, value: String) { @@ -171,6 +171,7 @@ object Starbound { .registerTypeAdapter(LeveledStatusEffect.ADAPTER) .registerTypeAdapter(MaterialReference.Companion) + .registerTypeAdapter(ThingDescription.ADAPTER) .registerTypeAdapter(ItemPrototype.ADAPTER) .registerTypeAdapter(CurrencyItemPrototype.ADAPTER) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt index 8c26f34b..dc6ebbb2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.defs +import ru.dbotthepony.kstarbound.io.json.FactoryAdapter + interface IThingWithDescription { /** * Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание", @@ -21,4 +23,35 @@ interface IThingWithDescription { * * The Poptop hums beautifully to confuse its prey. */ 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 get() = mapOf() + + /** + * Кратное описание штуки для определённых рас + */ + val racialShortDescription: Map 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() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt index adfb02eb..c37a4767 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemDefinition.kt @@ -1,8 +1,9 @@ package ru.dbotthepony.kstarbound.defs.item +import ru.dbotthepony.kstarbound.defs.IThingWithDescription +import ru.dbotthepony.kstarbound.defs.ThingDescription + data class ArmorItemDefinition( - override val shortdescription: String, - override val description: String, override val itemName: String, override val price: Long, override val rarity: ItemRarity, @@ -29,5 +30,7 @@ data class ArmorItemDefinition( override val armorType: ArmorPieceType, + val descriptionData: ThingDescription, + val json: Map, -) : IArmorItemDefinition +) : IArmorItemDefinition, IThingWithDescription by descriptionData diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt index 4acb2c26..637a4397 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt @@ -26,8 +26,7 @@ class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition { override fun assemble(): IItemDefinition { return ArmorItemDefinition( - shortdescription = shortdescription, - description = description, + descriptionData = descriptionData, itemName = itemName, price = price, rarity = rarity, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt index eaf7bf1f..a4f7ea46 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemDefinition.kt @@ -1,8 +1,9 @@ package ru.dbotthepony.kstarbound.defs.item +import ru.dbotthepony.kstarbound.defs.IThingWithDescription +import ru.dbotthepony.kstarbound.defs.ThingDescription + data class CurrencyItemDefinition( - override val shortdescription: String, - override val description: String, override val itemName: String, override val price: Long, override val rarity: ItemRarity, @@ -27,5 +28,7 @@ data class CurrencyItemDefinition( override val currency: String, override val value: Long, + val descriptionData: ThingDescription, + val json: Map, -) : ICurrencyItemDefinition +) : ICurrencyItemDefinition, IThingWithDescription by descriptionData diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt index 236ef3a9..27fdabf1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.defs.item +import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.enrollMap import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.util.NotNullVar @@ -19,8 +20,7 @@ class CurrencyItemPrototype : ItemPrototype(), ICurrencyItemDefinition { override fun assemble(): IItemDefinition { return CurrencyItemDefinition( - shortdescription = shortdescription, - description = description, + descriptionData = descriptionData, itemName = itemName, price = price, rarity = rarity, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt index b395d49e..6c3026b6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt @@ -1,8 +1,9 @@ package ru.dbotthepony.kstarbound.defs.item +import ru.dbotthepony.kstarbound.defs.IThingWithDescription +import ru.dbotthepony.kstarbound.defs.ThingDescription + data class ItemDefinition( - override val shortdescription: String, - override val description: String, override val itemName: String, override val price: Long, override val rarity: ItemRarity, @@ -19,5 +20,7 @@ data class ItemDefinition( override val radioMessagesOnPickup: List, override val fuelAmount: Long?, + val descriptionData: ThingDescription, + val json: Map -) : IItemDefinition +) : IItemDefinition, IThingWithDescription by descriptionData diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt index d2f05431..f66e9c09 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.defs.item +import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.enrollMap import ru.dbotthepony.kstarbound.io.json.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.INativeJsonHolder @@ -24,6 +25,8 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder { final override var radioMessagesOnPickup: List = listOf() final override var fuelAmount: Long? = null + var descriptionData: ThingDescription by NotNullVar() + var json: Map = mapOf() final override fun acceptJson(json: MutableMap) { @@ -32,8 +35,7 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder { open fun assemble(): IItemDefinition { return ItemDefinition( - shortdescription = shortdescription, - description = description, + descriptionData = descriptionData, itemName = itemName, price = price, rarity = rarity, @@ -78,6 +80,7 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder { auto(ItemPrototype::twoHanded) autoList(ItemPrototype::radioMessagesOnPickup) auto(ItemPrototype::fuelAmount) + flat(ItemPrototype::descriptionData) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemDefinition.kt index 40d69b27..6ea7727f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemDefinition.kt @@ -1,10 +1,10 @@ package ru.dbotthepony.kstarbound.defs.item +import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.MaterialReference +import ru.dbotthepony.kstarbound.defs.ThingDescription data class LiquidItemDefinition( - override val shortdescription: String, - override val description: String, override val itemName: String, override val price: Long, override val rarity: ItemRarity, @@ -23,5 +23,7 @@ data class LiquidItemDefinition( override val liquid: MaterialReference, + val descriptionData: ThingDescription, + val json: Map -) : ILiquidItem +) : ILiquidItem, IThingWithDescription by descriptionData diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemPrototype.kt index 24deefa0..741fad4d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemPrototype.kt @@ -20,8 +20,7 @@ class LiquidItemPrototype : ItemPrototype() { override fun assemble(): IItemDefinition { return LiquidItemDefinition( - shortdescription = shortdescription, - description = description, + descriptionData = descriptionData, itemName = itemName, price = price, rarity = rarity, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemDefinition.kt index 4cea69d1..c51221e0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemDefinition.kt @@ -1,10 +1,10 @@ package ru.dbotthepony.kstarbound.defs.item +import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.MaterialReference +import ru.dbotthepony.kstarbound.defs.ThingDescription data class MaterialItemDefinition( - override val shortdescription: String, - override val description: String, override val itemName: String, override val price: Long, override val rarity: ItemRarity, @@ -23,5 +23,7 @@ data class MaterialItemDefinition( override val material: MaterialReference, + val descriptionData: ThingDescription, + val json: Map -) : IMaterialItem +) : IMaterialItem, IThingWithDescription by descriptionData diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemPrototype.kt index 653bfa28..6924a111 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemPrototype.kt @@ -20,8 +20,7 @@ class MaterialItemPrototype : ItemPrototype() { override fun assemble(): IItemDefinition { return MaterialItemDefinition( - shortdescription = shortdescription, - description = description, + descriptionData = descriptionData, itemName = itemName, price = price, rarity = rarity, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt index c859d69e..ba2e6f87 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/BuilderAdapter.kt @@ -16,6 +16,7 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.flattenJsonElement import ru.dbotthepony.kstarbound.util.NotNullVar +import kotlin.properties.Delegates import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 import kotlin.reflect.full.isSubclassOf @@ -68,6 +69,11 @@ class BuilderAdapter private constructor( */ val properties: ImmutableMap>, + /** + * Свойства объекта [T], которые можно выставлять + */ + val flatProperties: ImmutableMap>, + /** * Ключи, которые необходимо игнорировать при чтении JSON */ @@ -77,6 +83,11 @@ class BuilderAdapter private constructor( * @see Builder.extraPropertiesAreFatal */ val extraPropertiesAreFatal: Boolean, + + /** + * @see Builder.logMisses + */ + val logMisses: Boolean, ) : TypeAdapter() { private val loggedMisses = ObjectOpenHashSet() @@ -92,15 +103,16 @@ class BuilderAdapter private constructor( missing.addAll(properties.values) val instance = factory.invoke() + var json: JsonObject by Delegates.notNull() - if (instance is IJsonHolder) { - val obj = TypeAdapters.JSON_ELEMENT.read(reader) - reader = JsonTreeReader(obj) + if (instance is IJsonHolder || flatProperties.isNotEmpty()) { + json = TypeAdapters.JSON_ELEMENT.read(reader) as JsonObject + reader = JsonTreeReader(json) if (instance is INativeJsonHolder) { - instance.acceptJson(flattenJsonElement(obj.asJsonObject, Starbound.STRING_INTERNER::intern)) - } else { - instance.acceptJson(obj.asJsonObject) + instance.acceptJson(flattenJsonElement(json, Starbound.STRING_INTERNER::intern)) + } else if (instance is IJsonHolder) { + instance.acceptJson(json) } } @@ -142,7 +154,7 @@ class BuilderAdapter private constructor( 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") loggedMisses.add(name) } @@ -153,9 +165,25 @@ class BuilderAdapter private constructor( 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) { 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) { if (property.returnType.isMarkedNullable) { continue @@ -214,8 +242,10 @@ class BuilderAdapter private constructor( class Builder(val factory: () -> T, vararg fields: KMutableProperty1) { private val properties = ArrayList>() + private val flatProperties = ArrayList>() private val ignoreKeys = ObjectArraySet() var extraPropertiesAreFatal = false + var logMisses = true /** * Являются ли "лишние" ключи в JSON структуре ошибкой. @@ -224,24 +254,45 @@ class BuilderAdapter private constructor( * то [extraPropertiesAreFatal] можно скомбинировать с [ignoreKey]. */ fun extraPropertiesAreFatal(flag: Boolean = true): Builder { + check(flatProperties.isEmpty() || !flag) { "Can't have both flattened properties and extraPropertiesAreFatal" } extraPropertiesAreFatal = flag return this } + /** + * Логировать ли несуществующие свойства у класса когда они попадаются в исходной JSON структуре + */ + fun logMisses(flag: Boolean = true): Builder { + logMisses = flag + return this + } + init { for (field in fields) auto(field) } - fun add(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit = {}): Builder { + private fun _add(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit): PropertyConfigurator { if (properties.any { it.property == property }) { throw IllegalArgumentException("Property $property is defined twice") } + if (flatProperties.any { it.property == property }) { + throw IllegalArgumentException("Property $property is defined twice") + } + ignoreKeys.remove(property.name) val config = PropertyConfigurator(property, adapter) configurator.invoke(config) + return config + } + + /** + * Добавляет указанное свойство в будущий адаптер с указанным [adapter] + */ + fun add(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit = {}): Builder { + val config = _add(property, adapter, configurator) properties.add(WrappedProperty( property, @@ -252,6 +303,38 @@ class BuilderAdapter private constructor( return this } + /** + * Добавляет указанное свойство в будущий адаптер, как плоский объект внутри данного на том же уровне, что и сам объект, используя адаптер [adapter] + * + * Пример: + * ```json + * { + * "prop_belong_to_a_1": ..., + * "prop_belong_to_a_2": ..., + * "prop_belong_to_b_1": ..., + * } + * ``` + * + * В данном случае, можно указать `b` как плоский класс внутри `a`. + * + * Данный подход позволяет избавиться от постоянного наследования и реализации одного и того же интерфейса во множестве других классов. + * + * Флаг [extraPropertiesAreFatal] не поддерживается с данными свойствами + */ + fun flat(property: KMutableProperty1, adapter: TypeAdapter, configurator: PropertyConfigurator.() -> Unit = {}): Builder { + 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] */ @@ -271,6 +354,25 @@ class BuilderAdapter private constructor( return add(property, LazyTypeProvider(classifier.java) as TypeAdapter, configurator) } + /** + * Автоматически определяет тип свойства и необходимый [TypeAdapter], инкапсулированный в данный типе на одном уровне + */ + fun flat(property: KMutableProperty1, configurator: PropertyConfigurator.() -> Unit = {}): Builder { + 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, configurator) + } + /** * Автоматически создаёт [ListAdapter] для заданного свойства */ @@ -300,11 +402,18 @@ class BuilderAdapter private constructor( for (property in properties) map.put(property.property.name, property) + val map2 = ImmutableMap.Builder>() + + for (property in flatProperties) + map2.put(property.property.name, property) + return BuilderAdapter( factory = factory, properties = map.build(), + flatProperties = map2.build(), ignoreKeys = ImmutableSet.copyOf(ignoreKeys), extraPropertiesAreFatal = extraPropertiesAreFatal, + logMisses = logMisses, ) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt index 9375fb67..d5218e66 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/FactoryAdapter.kt @@ -23,18 +23,11 @@ import ru.dbotthepony.kstarbound.getValue import ru.dbotthepony.kstarbound.setValue import java.lang.reflect.Constructor import kotlin.jvm.internal.DefaultConstructorMarker +import kotlin.properties.Delegates import kotlin.reflect.* import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSupertypeOf -private data class PackedProperty( - val property: KProperty1, - val adapter: TypeAdapter, - val transformer: (T) -> T = { it } -) { - val returnType = property.returnType -} - /** * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе. */ @@ -44,11 +37,18 @@ class FactoryAdapter private constructor( val asJsonArray: Boolean, val storesJson: Boolean ) : TypeAdapter() { + private data class PackedProperty( + val property: KProperty1, + val adapter: TypeAdapter, + val transformer: (T) -> T = { it }, + val isFlat: Boolean + ) { + val returnType = property.returnType + } + private val name2index = Object2IntArrayMap() private val loggedMisses = ObjectArraySet() - var currentSymbolicName by ThreadLocal() - init { name2index.defaultReturnValue(-1) @@ -218,20 +218,24 @@ class FactoryAdapter private constructor( readValues[fieldId] = (tuple.transformer as (Any?) -> Any?)(adapter.read(reader)) presentValues[fieldId] = true } 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++ } // иначе - читаем как json object } 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) if (readMap !is JsonObject) { throw JsonParseException("Expected JSON element to be a Map, ${readMap::class.qualifiedName} given") } + json = readMap reader = JsonTreeReader(readMap) readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap) as Map, Starbound.STRING_INTERNER::intern) } @@ -243,7 +247,7 @@ class FactoryAdapter private constructor( val fieldId = name2index.getInt(name) if (fieldId == -1) { - if (!storesJson && loggedMisses.add(name)) { + if (!storesJson && !hasFlatValues && loggedMisses.add(name)) { if (currentSymbolicName == null) { LOGGER.warn("${bound.qualifiedName} has no property for storing $name ") } else { @@ -254,17 +258,34 @@ class FactoryAdapter private constructor( reader.skipValue() } else { val tuple = types[fieldId] + + if (tuple.isFlat) { + reader.skipValue() + continue + } + val (field, adapter) = tuple try { readValues[fieldId] = (tuple.transformer as (Any?) -> Any?)(adapter.read(reader)) presentValues[fieldId] = true } catch(err: Throwable) { - if (currentSymbolicName == null) { - 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) + throw JsonSyntaxException("Reading field ${field.name} for ${bound.qualifiedName}", 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 private constructor( } /** - * Добавляет поле с определённым адаптером + * Добавляет свойство с определённым [adapter] */ fun add(field: KProperty1, adapter: TypeAdapter): Builder { - types.add(PackedProperty(field, adapter)) + types.add(PackedProperty(field, adapter, isFlat = false)) + return this + } + + /** + * Добавляет свойство с определённым [adapter], которое находится на том же уровне что и данный объект внутри JSON структуры + */ + fun flat(field: KProperty1, adapter: TypeAdapter): Builder { + types.add(PackedProperty(field, adapter, isFlat = true)) return this } @@ -399,6 +428,17 @@ class FactoryAdapter private constructor( return this } + /** + * Добавляет поля без generic типов и без преобразователей + */ + @Suppress("unchecked_cast") + fun autoFlat(vararg fields: KProperty1): Builder { + for (field in fields) + autoFlat(field) + + return this + } + /** * Автоматически определяет тип поля и необходимый адаптор типа к нему */ @@ -415,7 +455,27 @@ class FactoryAdapter private constructor( throw IllegalArgumentException("${field.name} is a Map, please use autoMap() method instead") } - types.add(PackedProperty(field, LazyTypeProvider(classifier.java) as TypeAdapter, transformer = transformer as (Any?) -> Any?)) + types.add(PackedProperty(field, LazyTypeProvider(classifier.java) as TypeAdapter, transformer = transformer as (Any?) -> Any?, isFlat = false)) + return this + } + + /** + * Автоматически определяет тип поля и необходимый адаптор типа к нему + */ + @Suppress("unchecked_cast") + fun autoFlat(field: KProperty1, transformer: (In) -> In = { it }): Builder { + 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, transformer = transformer as (Any?) -> Any?, isFlat = true)) return this } @@ -425,7 +485,7 @@ class FactoryAdapter private constructor( * Список неизменяем (создаётся объект [ImmutableList]) */ fun list(field: KProperty1?>, type: Class, transformer: (V) -> V = { it }): Builder { - types.add(PackedProperty(field, ListAdapter(type, transformer).nullSafe())) + types.add(PackedProperty(field, ListAdapter(type, transformer).nullSafe(), isFlat = false)) return this } @@ -444,7 +504,7 @@ class FactoryAdapter private constructor( * Таблица неизменяема (создаётся объект [ImmutableMap]) */ fun mapAsArray(field: KProperty1>, keyType: Class, valueType: Class): Builder { - types.add(PackedProperty(field, MapAdapter(keyType, valueType))) + types.add(PackedProperty(field, MapAdapter(keyType, valueType), isFlat = false)) return this } @@ -468,7 +528,7 @@ class FactoryAdapter private constructor( * Таблица неизменяема (создаётся объект [ImmutableMap]) */ fun mapAsArray(field: KProperty1?>, keyType: KClass, valueType: KClass): Builder { - types.add(PackedProperty(field, MapAdapter(keyType.java, valueType.java).nullSafe())) + types.add(PackedProperty(field, MapAdapter(keyType.java, valueType.java).nullSafe(), isFlat = false)) return this } @@ -478,7 +538,7 @@ class FactoryAdapter private constructor( * Таблица неизменяема (создаётся объект [ImmutableMap]) */ fun mapAsObject(field: KProperty1?>, valueType: Class): Builder { - types.add(PackedProperty(field, String2ObjectAdapter(valueType).nullSafe())) + types.add(PackedProperty(field, String2ObjectAdapter(valueType).nullSafe(), isFlat = false)) return this } @@ -488,7 +548,7 @@ class FactoryAdapter private constructor( * Таблица неизменяема (создаётся объект [ImmutableMap]) */ fun mapAsObject(field: KProperty1?>, valueType: KClass): Builder { - types.add(PackedProperty(field, String2ObjectAdapter(valueType.java).nullSafe())) + types.add(PackedProperty(field, String2ObjectAdapter(valueType.java).nullSafe(), isFlat = false)) return this } @@ -505,16 +565,20 @@ class FactoryAdapter private constructor( } fun build(): FactoryAdapter { + check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" } + return FactoryAdapter( bound = clazz, types = ImmutableList.copyOf(types), asJsonArray = asList, - storesJson = storesJson + storesJson = storesJson, ) } } companion object { private val LOGGER = LogManager.getLogger() + + var currentSymbolicName by ThreadLocal() } }