From 6bcf5049083cd565ee7d2cda4f6976000edac5c6 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 23 Jan 2023 13:26:12 +0700 Subject: [PATCH] =?UTF-8?q?=D0=95=D1=89=D1=91=20=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B5=20json=20=D0=B0=D0=BD=D0=BD=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B9,=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20FactoryAdapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/dbotthepony/kstarbound/Starbound.kt | 11 +++- .../kstarbound/defs/IThingWithDescription.kt | 2 + .../kstarbound/defs/item/IItemDefinition.kt | 2 + .../kstarbound/defs/item/ItemDefinition.kt | 6 ++- .../kstarbound/io/json/builder/Annotations.kt | 45 ++++++++++++++++ .../io/json/builder/FactoryAdapter.kt | 54 +++++++++++++++++-- 6 files changed, 115 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index c078f26e..8e90df55 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.item.CurrencyItemPrototype import ru.dbotthepony.kstarbound.defs.item.IArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.IFossilItemDefinition import ru.dbotthepony.kstarbound.defs.item.IItemDefinition +import ru.dbotthepony.kstarbound.defs.item.ItemDefinition import ru.dbotthepony.kstarbound.defs.item.ItemPrototype import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.defs.item.ItemTooltipKind @@ -47,6 +48,8 @@ import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter +import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter +import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementationTypeFactory import ru.dbotthepony.kstarbound.io.json.factory.ArrayListAdapterFactory import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.math.* @@ -151,6 +154,9 @@ object Starbound { // чтоб строки всегда intern'ились .registerTypeAdapter(NULLABLE_STRING_ADAPTER) + // Обработчик @JsonImplementation + .registerTypeAdapterFactory(JsonImplementationTypeFactory) + // ImmutableList, ImmutableSet, ImmutableMap .registerTypeAdapterFactory(ImmutableCollectionAdapterFactory) @@ -160,9 +166,12 @@ object Starbound { // все enum'ы без особых настроек .registerTypeAdapterFactory(EnumAdapter.Companion) - // автоматическое создание BuilderAdapter + // автоматическое создание BuilderAdapter по @аннотациям .registerTypeAdapterFactory(BuilderAdapter.Companion) + // автоматическое создание FactoryAdapter по @аннотациям + .registerTypeAdapterFactory(FactoryAdapter.Companion) + .also(::addStarboundJsonAdapters) .create() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt index eb5d1bae..aaf84317 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt @@ -5,9 +5,11 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementation import ru.dbotthepony.kstarbound.sbIntern import ru.dbotthepony.kstarbound.util.NotNullVar +@JsonImplementation(ThingDescription::class) interface IThingWithDescription { /** * Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание", diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt index b77ef64e..d26709ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IItemDefinition.kt @@ -4,6 +4,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter +import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementation import ru.dbotthepony.kstarbound.io.json.ifString interface IItemDefinition : IThingWithDescription { @@ -36,6 +37,7 @@ interface IItemDefinition : IThingWithDescription { */ val inventoryIcon: List? + @JsonImplementation(InventoryIcon::class) interface IInventoryIcon { val image: SpriteReference } 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 1dfaf698..0ca5c667 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDefinition.kt @@ -3,10 +3,13 @@ package ru.dbotthepony.kstarbound.defs.item import com.google.common.collect.ImmutableList import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.ThingDescription +import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.io.json.builder.JsonPropertyConfig +@JsonFactory(storesJson = true) data class ItemDefinition( override val itemName: String, - override val price: Long, + override val price: Long = 4L, override val rarity: ItemRarity, override val category: String?, override val inventoryIcon: ImmutableList?, @@ -21,6 +24,7 @@ data class ItemDefinition( override val radioMessagesOnPickup: ImmutableList, override val fuelAmount: Long?, + @JsonPropertyConfig(isFlat = true) val descriptionData: ThingDescription, val json: Map diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/Annotations.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/Annotations.kt index 9b94a26c..ba19f702 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/Annotations.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/Annotations.kt @@ -1,5 +1,11 @@ package ru.dbotthepony.kstarbound.io.json.builder +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import kotlin.reflect.KClass + private fun Int.toBool() = if (this == 0) null else this > 0 /** @@ -60,6 +66,14 @@ val JsonPropertyConfig.realMustBePresent get() = mustBePresent.toBool() /** * Указывает, что для данного класса можно автоматически создать [FactoryAdapter] + * + * В подавляющем большинстве случаев это работает исключительно с data классами + * + * С технической точки зрения, у класса определяются все свойства и происходит поиск *главного* конструктора, а + * затем аргументы конструктора отражаются на свойства класса + * + * @see JsonIgnoreProperty + * @see JsonPropertyConfig */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @@ -80,3 +94,34 @@ annotation class JsonFactory( */ val asList: Boolean = false ) + +/** + * Позволяет указать, какую реализацию использовать при попытке разобрать JSON структуру + * с аннотированным типом + * + * Пример: + * ```kotlin + * @JsonFactory + * data class Item(val thingDef: IThing, val price: Long) + * + * @JsonImplementation(Thing::class) + * interface IThing { val prop: String } + * + * data class Thing(override val prop: String) : IThing + * ``` + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class JsonImplementation(val implementingClass: KClass<*>) + +object JsonImplementationTypeFactory : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + val delegate = type.rawType.getAnnotation(JsonImplementation::class.java) + + if (delegate != null) { + return gson.getAdapter(delegate.implementingClass.java) as TypeAdapter? + } + + return null + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt index e0a6b57a..97c45675 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/FactoryAdapter.kt @@ -33,8 +33,12 @@ import java.lang.reflect.Constructor import kotlin.jvm.internal.DefaultConstructorMarker import kotlin.properties.Delegates import kotlin.reflect.* +import kotlin.reflect.full.declaredMembers +import kotlin.reflect.full.hasAnnotation import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.isSubtypeOf import kotlin.reflect.full.isSupertypeOf +import kotlin.reflect.full.primaryConstructor /** * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе. @@ -452,8 +456,8 @@ class FactoryAdapter private constructor( * * На самом деле, JSON "заворачивается" в [ImmutableMap], или [ImmutableList] если указано [asList]/[inputAsList] * - * Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] первым аргументом, - * иначе поиск конструктора завершится неудчаей + * Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] последним аргументом, + * иначе поиск конструктора завершится неудачей */ fun storesJson(flag: Boolean = true): Builder { storesJson = flag @@ -524,9 +528,53 @@ class FactoryAdapter private constructor( } } - companion object { + companion object : TypeAdapterFactory { private val LOGGER = LogManager.getLogger() var currentSymbolicName by ThreadLocal() + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + val raw = type.rawType + + if (raw.isAnnotationPresent(JsonFactory::class.java)) { + val first = raw.getAnnotationsByType(JsonFactory::class.java) + require(first.size == 1) { "Multiple JsonFactory defined: ${first.joinToString(", ")}" } + + val bconfig = first[0] as JsonFactory + val kclass = raw.kotlin as KClass + val builder = Builder(kclass) + val properties = kclass.declaredMembers.filterIsInstance>() + + if (bconfig.asList) { + builder.inputAsList() + } + + builder.storesJson(bconfig.storesJson) + builder.logMisses(bconfig.logMisses) + + if (properties.isEmpty()) { + throw IllegalArgumentException("${kclass.qualifiedName} has no valid members") + } + + val foundConstructor = kclass.primaryConstructor ?: throw NoSuchElementException("Can't determine primary constructor for ${kclass.qualifiedName}") + + if (!bconfig.storesJson) { + for (argument in foundConstructor.parameters) { + builder.auto(properties.first { it.name == argument.name && it.returnType.isSupertypeOf(argument.type) }) + } + } else { + val params = foundConstructor.parameters + + for (i in 0 until params.size - 1) { + val argument = params[i] + builder.auto(properties.first { it.name == argument.name && it.returnType.isSupertypeOf(argument.type) }) + } + } + + return builder.build(gson) + } + + return null + } } }