From 20bd844f2304761eddd5bb13637637372ebd2d6e Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 23 Jan 2023 12:11:28 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20BuilderAdapter=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D0=BE=D1=82=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/dbotthepony/kstarbound/Starbound.kt | 4 + .../kstarbound/StarboundJsonAdapters.kt | 5 -- .../defs/item/ArmorItemPrototype.kt | 16 +--- .../defs/item/CurrencyItemPrototype.kt | 14 +--- .../kstarbound/defs/item/ItemPrototype.kt | 37 ++------- .../defs/item/LiquidItemPrototype.kt | 10 +-- .../defs/item/MaterialItemPrototype.kt | 10 +-- .../kstarbound/io/json/builder/Annotations.kt | 82 +++++++++++++++++++ .../io/json/builder/BuilderAdapter.kt | 61 ++++++++++++-- .../io/json/builder/FactoryAdapter.kt | 37 +++++++-- 10 files changed, 188 insertions(+), 88 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/Annotations.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 8a150f4a..c078f26e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -46,6 +46,7 @@ import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter 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.factory.ArrayListAdapterFactory import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.math.* @@ -159,6 +160,9 @@ object Starbound { // все enum'ы без особых настроек .registerTypeAdapterFactory(EnumAdapter.Companion) + // автоматическое создание BuilderAdapter + .registerTypeAdapterFactory(BuilderAdapter.Companion) + .also(::addStarboundJsonAdapters) .create() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundJsonAdapters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundJsonAdapters.kt index f31de8e4..66fcef9a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundJsonAdapters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundJsonAdapters.kt @@ -93,11 +93,6 @@ fun addStarboundJsonAdapters(builder: GsonBuilder) { registerTypeAdapter(ParallaxPrototypeLayer.LAYER_PARALLAX_ADAPTER) // Предметы - registerTypeAdapterFactory(ItemPrototype.ADAPTER) - registerTypeAdapterFactory(CurrencyItemPrototype.ADAPTER) - registerTypeAdapterFactory(ArmorItemPrototype.ADAPTER) - registerTypeAdapterFactory(MaterialItemPrototype.ADAPTER) - registerTypeAdapterFactory(LiquidItemPrototype.ADAPTER) registerTypeAdapterFactory(IItemDefinition.InventoryIcon.ADAPTER) registerTypeAdapterFactory(IFossilItemDefinition.FossilSetDescription.ADAPTER) registerTypeAdapterFactory(IArmorItemDefinition.ArmorFrames.ADAPTER) 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 d0e9fb7c..0c5215a5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ArmorItemPrototype.kt @@ -4,11 +4,14 @@ import com.google.common.collect.ImmutableList import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.util.enrollMap import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter +import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder +import ru.dbotthepony.kstarbound.io.json.builder.JsonIgnoreProperty import ru.dbotthepony.kstarbound.io.json.util.asJsonObject import ru.dbotthepony.kstarbound.io.json.util.asList import ru.dbotthepony.kstarbound.io.json.neverNull import ru.dbotthepony.kstarbound.util.NotNullVar +@JsonBuilder class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition { override var colorOptions: ImmutableList> = ImmutableList.of() override var maleFrames: IArmorItemDefinition.ArmorFrames by NotNullVar() @@ -19,6 +22,7 @@ class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition { override var scripts: ImmutableList = ImmutableList.of() override var scriptDelta: Int = 1 + @JsonIgnoreProperty override var armorType: ArmorPieceType by NotNullVar() init { @@ -57,16 +61,4 @@ class ArmorItemPrototype : ItemPrototype(), IArmorItemDefinition { armorType = armorType, ) } - - companion object { - val ADAPTER = BuilderAdapter.Builder(::ArmorItemPrototype) - .also { addFields(it as BuilderAdapter.Builder) } // безопасность: свойства родительского класса объявлены как final - .auto(ArmorItemPrototype::colorOptions) - .auto(ArmorItemPrototype::maleFrames) - .auto(ArmorItemPrototype::femaleFrames) - .auto(ArmorItemPrototype::level) - .auto(ArmorItemPrototype::leveledStatusEffects) - .auto(ArmorItemPrototype::scripts) - .auto(ArmorItemPrototype::scriptDelta) - } } 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 1a5b1b00..6c574ac1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/CurrencyItemPrototype.kt @@ -3,8 +3,10 @@ package ru.dbotthepony.kstarbound.defs.item import com.google.common.collect.ImmutableList import ru.dbotthepony.kstarbound.defs.util.enrollMap import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter +import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder import ru.dbotthepony.kstarbound.util.NotNullVar +@JsonBuilder class CurrencyItemPrototype : ItemPrototype(), ICurrencyItemDefinition { override var pickupSoundsSmall: ImmutableList = ImmutableList.of() override var pickupSoundsMedium: ImmutableList = ImmutableList.of() @@ -48,16 +50,4 @@ class CurrencyItemPrototype : ItemPrototype(), ICurrencyItemDefinition { value = value, ) } - - companion object { - val ADAPTER = BuilderAdapter.Builder(::CurrencyItemPrototype) - .also { addFields(it as BuilderAdapter.Builder) } // безопасность: свойства родительского класса объявлены как final - .auto(CurrencyItemPrototype::pickupSoundsSmall) - .auto(CurrencyItemPrototype::pickupSoundsMedium) - .auto(CurrencyItemPrototype::pickupSoundsLarge) - .auto(CurrencyItemPrototype::smallStackLimit) - .auto(CurrencyItemPrototype::mediumStackLimit) - .auto(CurrencyItemPrototype::currency) - .auto(CurrencyItemPrototype::value) - } } 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 1bbb75c0..bf4f9e13 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemPrototype.kt @@ -5,11 +5,18 @@ import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.util.enrollMap import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter import ru.dbotthepony.kstarbound.io.json.builder.INativeJsonHolder +import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder +import ru.dbotthepony.kstarbound.io.json.builder.JsonPropertyConfig +import ru.dbotthepony.kstarbound.io.json.builder.JsonIgnoreProperty import ru.dbotthepony.kstarbound.util.NotNullVar +@JsonBuilder open class ItemPrototype : IItemDefinition, INativeJsonHolder { + @JsonIgnoreProperty final override var shortdescription: String = "..." + @JsonIgnoreProperty final override var description: String = "..." + final override var itemName: String by NotNullVar() final override var price: Long = 0L final override var rarity: ItemRarity = ItemRarity.COMMON @@ -26,8 +33,10 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder { final override var radioMessagesOnPickup: ImmutableList = ImmutableList.of() final override var fuelAmount: Long? = null + @JsonPropertyConfig(isFlat = true) var descriptionData: ThingDescription by NotNullVar() + @JsonIgnoreProperty var json: Map = mapOf() final override fun acceptJson(json: MutableMap) { @@ -56,32 +65,4 @@ open class ItemPrototype : IItemDefinition, INativeJsonHolder { json = enrollMap(json), ) } - - companion object { - val ADAPTER = BuilderAdapter.Builder(::ItemPrototype) - .also(::addFields) - - fun addFields(builder: BuilderAdapter.Builder) { - with(builder) { - auto(ItemPrototype::shortdescription) - auto(ItemPrototype::description) - auto(ItemPrototype::itemName) - auto(ItemPrototype::price) - auto(ItemPrototype::rarity) - auto(ItemPrototype::category) - auto(ItemPrototype::inventoryIcon) - auto(ItemPrototype::itemTags) - auto(ItemPrototype::learnBlueprintsOnPickup) - auto(ItemPrototype::maxStack) - auto(ItemPrototype::eventCategory) - auto(ItemPrototype::consumeOnPickup) - auto(ItemPrototype::pickupQuestTemplates) - auto(ItemPrototype::tooltipKind) - auto(ItemPrototype::twoHanded) - auto(ItemPrototype::radioMessagesOnPickup) - auto(ItemPrototype::fuelAmount) - auto(ItemPrototype::descriptionData, isFlat = true) - } - } - } } 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 f7295d21..e0073255 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/LiquidItemPrototype.kt @@ -3,7 +3,9 @@ package ru.dbotthepony.kstarbound.defs.item import ru.dbotthepony.kstarbound.defs.MaterialReference import ru.dbotthepony.kstarbound.defs.util.enrollMap import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter +import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder +@JsonBuilder class LiquidItemPrototype : ItemPrototype() { var liquid: MaterialReference? = null @@ -42,12 +44,4 @@ class LiquidItemPrototype : ItemPrototype() { json = enrollMap(json), ) } - - companion object { - val ADAPTER = BuilderAdapter.Builder(::LiquidItemPrototype) - .also { addFields(it as BuilderAdapter.Builder) } // безопасность: свойства родительского класса объявлены как final - .auto(LiquidItemPrototype::liquid) - .auto(LiquidItemPrototype::liquidId) - .auto(LiquidItemPrototype::liquidName) - } } 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 2a337350..3ec4f453 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemPrototype.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/MaterialItemPrototype.kt @@ -3,7 +3,9 @@ package ru.dbotthepony.kstarbound.defs.item import ru.dbotthepony.kstarbound.defs.MaterialReference import ru.dbotthepony.kstarbound.defs.util.enrollMap import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter +import ru.dbotthepony.kstarbound.io.json.builder.JsonBuilder +@JsonBuilder class MaterialItemPrototype : ItemPrototype() { var material: MaterialReference? = null @@ -42,12 +44,4 @@ class MaterialItemPrototype : ItemPrototype() { json = enrollMap(json), ) } - - companion object { - val ADAPTER = BuilderAdapter.Builder(::MaterialItemPrototype) - .also { addFields(it as BuilderAdapter.Builder) } // безопасность: свойства родительского класса объявлены как final - .auto(MaterialItemPrototype::material) - .auto(MaterialItemPrototype::materialId) - .auto(MaterialItemPrototype::materialName) - } } 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 new file mode 100644 index 00000000..9b94a26c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/Annotations.kt @@ -0,0 +1,82 @@ +package ru.dbotthepony.kstarbound.io.json.builder + +private fun Int.toBool() = if (this == 0) null else this > 0 + +/** + * Указывает, что для данного класса можно автоматически создать [BuilderAdapter] для всех его свойств, + * которые не указаны как [JsonIgnoreProperty] + * + * @see JsonIgnoreProperty + * @see JsonPropertyConfig + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class JsonBuilder( + /** + * @see BuilderAdapter.Builder.extraPropertiesAreFatal + */ + val extraPropertiesAreFatal: Boolean = false, + + /** + * @see BuilderAdapter.Builder.ignoreKey + */ + val ignoreKeys: Array = [], + + /** + * 0 = null + * -1 = false + * 1 = true + * + * @see BuilderAdapter.Builder.logMisses + */ + val logMisses: Int = 0, + + val includeSuperclassProperties: Boolean = true, +) + +val JsonBuilder.realLogMisses get() = logMisses.toBool() + +/** + * Заставляет указанное свойство быть проигнорированным при автоматическом создании [BuilderAdapter] + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class JsonIgnoreProperty + +/** + * Выставляет флаги данному свойству при автоматическом создании [BuilderAdapter] + * + * @see BuilderAdapter.Builder.add + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class JsonPropertyConfig( + val isFlat: Boolean = false, + + val mustBePresent: Int = 0, +) + +val JsonPropertyConfig.realMustBePresent get() = mustBePresent.toBool() + +/** + * Указывает, что для данного класса можно автоматически создать [FactoryAdapter] + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class JsonFactory( + /** + * @see FactoryAdapter.Builder.storesJson + */ + val storesJson: Boolean = false, + + /** + * @see FactoryAdapter.Builder.logMisses + */ + val logMisses: Boolean = true, + + /** + * @see FactoryAdapter.Builder.inputAsList + * @see FactoryAdapter.Builder.inputAsMap + */ + val asList: Boolean = false +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt index 43b24d8c..29ab7289 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/builder/BuilderAdapter.kt @@ -18,14 +18,13 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.util.flattenJsonElement -import ru.dbotthepony.kstarbound.io.json.util.LazyTypeProvider -import ru.dbotthepony.kstarbound.io.json.util.ListAdapter import ru.dbotthepony.kstarbound.util.NotNullVar import kotlin.properties.Delegates +import kotlin.reflect.KCallable import kotlin.reflect.KClass import kotlin.reflect.KMutableProperty1 -import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.javaType +import kotlin.reflect.full.declaredMembers +import kotlin.reflect.jvm.isAccessible /** * Данный интерфейс имеет один единственный метод: [acceptJson] @@ -216,7 +215,6 @@ class BuilderAdapter private constructor( var logMisses: Boolean? = null private val factoryReturnType by lazy { factory.invoke()::class.java } - @OptIn(ExperimentalStdlibApi::class) override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == factoryReturnType) { return build(gson) as TypeAdapter @@ -348,7 +346,58 @@ class BuilderAdapter private constructor( } } - companion object { + companion object : TypeAdapterFactory { private val LOGGER = LogManager.getLogger() + + private fun collectDecl(input: KClass<*>, output: MutableMap>) { + for (decl in input.declaredMembers) { + if (decl is KMutableProperty1<*, *>) { + decl.isAccessible = true + output.putIfAbsent(decl.name, decl) + } + } + + for (parent in input.supertypes) { + if (parent.classifier is KClass<*>) { + collectDecl(parent.classifier as KClass<*>, output) + } + } + } + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + val raw = type.rawType + + if (raw.isAnnotationPresent(JsonBuilder::class.java)) { + val first = raw.getAnnotationsByType(JsonBuilder::class.java) + require(first.size == 1) { "Multiple JsonBuilder defined: ${first.joinToString(", ")}" } + + val bconfig = first[0] as JsonBuilder + val kclass = raw.kotlin + val builder = Builder(kclass.constructors.first { it.parameters.isEmpty() && !it.returnType.isMarkedNullable } as () -> T) + + builder.logMisses = bconfig.realLogMisses + builder.extraPropertiesAreFatal = bconfig.extraPropertiesAreFatal + + val declarations = LinkedHashMap>() + collectDecl(kclass, declarations) + + for (decl in declarations.values) { + if (decl.annotations.none { it is JsonIgnoreProperty }) { + val config = decl.annotations.firstOrNull { it is JsonPropertyConfig } + + if (config == null) { + builder.auto(decl as KMutableProperty1) + } else { + config as JsonPropertyConfig + builder.auto(decl as KMutableProperty1, isFlat = config.isFlat, mustBePresent = config.realMustBePresent) + } + } + } + + return builder.build(gson) + } + + 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 70c46cee..e0a6b57a 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 @@ -384,6 +384,8 @@ class FactoryAdapter private constructor( } } + private var storesJson = false + private var logMisses = true private val types = ArrayList>() private var stringTransformer: ((String) -> T)? = null @@ -453,13 +455,6 @@ class FactoryAdapter private constructor( * Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] первым аргументом, * иначе поиск конструктора завершится неудчаей */ - var storesJson = false - - /** - * Логировать ли несуществующие свойства у класса когда они попадаются в исходной JSON структуре - */ - var logMisses = true - fun storesJson(flag: Boolean = true): Builder { storesJson = flag return this @@ -482,7 +477,7 @@ class FactoryAdapter private constructor( } /** - * Автоматически определяет тип поля и необходимый адаптор типа к нему + * Автоматически определяет необходимый адаптер типа к свойству при сборке данного адаптера внутри Gson * * Можно указать [transform] для изменения определённого адаптера */ @@ -492,13 +487,37 @@ class FactoryAdapter private constructor( return this } - var asList = false + private var asList = false + /** + * При выставлении данного флага в качестве исходной структуры будет приниматься Json объект: + * + * ```json + * { + * "thingname": "a", + * "count": 4, + * ... + * } + * ``` + * + * Данный режим используется по умолчанию + * + * @see inputAsList + */ fun inputAsMap(): Builder { asList = false return this } + /** + * При выставлении данного флага в качестве исходной структуры будет приниматься Json массив: + * + * ```json + * ["a", 4, 100.2, true] + * ``` + * + * @see inputAsMap + */ fun inputAsList(): Builder { asList = true return this