Ещё больше json аннотаций, теперь для FactoryAdapter

This commit is contained in:
DBotThePony 2023-01-23 13:26:12 +07:00
parent f7c8455b87
commit 6bcf504908
Signed by: DBot
GPG Key ID: DCC23B5715498507
6 changed files with 115 additions and 5 deletions

View File

@ -23,6 +23,7 @@ import ru.dbotthepony.kstarbound.defs.item.CurrencyItemPrototype
import ru.dbotthepony.kstarbound.defs.item.IArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.IArmorItemDefinition
import ru.dbotthepony.kstarbound.defs.item.IFossilItemDefinition import ru.dbotthepony.kstarbound.defs.item.IFossilItemDefinition
import ru.dbotthepony.kstarbound.defs.item.IItemDefinition 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.ItemPrototype
import ru.dbotthepony.kstarbound.defs.item.ItemRarity import ru.dbotthepony.kstarbound.defs.item.ItemRarity
import ru.dbotthepony.kstarbound.defs.item.ItemTooltipKind 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.Vector2iTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector4iTypeAdapter
import ru.dbotthepony.kstarbound.io.json.builder.BuilderAdapter 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.ArrayListAdapterFactory
import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory import ru.dbotthepony.kstarbound.io.json.factory.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
@ -151,6 +154,9 @@ object Starbound {
// чтоб строки всегда intern'ились // чтоб строки всегда intern'ились
.registerTypeAdapter(NULLABLE_STRING_ADAPTER) .registerTypeAdapter(NULLABLE_STRING_ADAPTER)
// Обработчик @JsonImplementation
.registerTypeAdapterFactory(JsonImplementationTypeFactory)
// ImmutableList, ImmutableSet, ImmutableMap // ImmutableList, ImmutableSet, ImmutableMap
.registerTypeAdapterFactory(ImmutableCollectionAdapterFactory) .registerTypeAdapterFactory(ImmutableCollectionAdapterFactory)
@ -160,9 +166,12 @@ object Starbound {
// все enum'ы без особых настроек // все enum'ы без особых настроек
.registerTypeAdapterFactory(EnumAdapter.Companion) .registerTypeAdapterFactory(EnumAdapter.Companion)
// автоматическое создание BuilderAdapter // автоматическое создание BuilderAdapter по @аннотациям
.registerTypeAdapterFactory(BuilderAdapter.Companion) .registerTypeAdapterFactory(BuilderAdapter.Companion)
// автоматическое создание FactoryAdapter по @аннотациям
.registerTypeAdapterFactory(FactoryAdapter.Companion)
.also(::addStarboundJsonAdapters) .also(::addStarboundJsonAdapters)
.create() .create()

View File

@ -5,9 +5,11 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementation
import ru.dbotthepony.kstarbound.sbIntern import ru.dbotthepony.kstarbound.sbIntern
import ru.dbotthepony.kstarbound.util.NotNullVar import ru.dbotthepony.kstarbound.util.NotNullVar
@JsonImplementation(ThingDescription::class)
interface IThingWithDescription { interface IThingWithDescription {
/** /**
* Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание", * Краткое описание штуки. Несмотря на то, что название свойства подразумевает "описание",

View File

@ -4,6 +4,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.io.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.io.json.builder.JsonImplementation
import ru.dbotthepony.kstarbound.io.json.ifString import ru.dbotthepony.kstarbound.io.json.ifString
interface IItemDefinition : IThingWithDescription { interface IItemDefinition : IThingWithDescription {
@ -36,6 +37,7 @@ interface IItemDefinition : IThingWithDescription {
*/ */
val inventoryIcon: List<IInventoryIcon>? val inventoryIcon: List<IInventoryIcon>?
@JsonImplementation(InventoryIcon::class)
interface IInventoryIcon { interface IInventoryIcon {
val image: SpriteReference val image: SpriteReference
} }

View File

@ -3,10 +3,13 @@ package ru.dbotthepony.kstarbound.defs.item
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.defs.IThingWithDescription import ru.dbotthepony.kstarbound.defs.IThingWithDescription
import ru.dbotthepony.kstarbound.defs.ThingDescription 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( data class ItemDefinition(
override val itemName: String, override val itemName: String,
override val price: Long, override val price: Long = 4L,
override val rarity: ItemRarity, override val rarity: ItemRarity,
override val category: String?, override val category: String?,
override val inventoryIcon: ImmutableList<out IItemDefinition.IInventoryIcon>?, override val inventoryIcon: ImmutableList<out IItemDefinition.IInventoryIcon>?,
@ -21,6 +24,7 @@ data class ItemDefinition(
override val radioMessagesOnPickup: ImmutableList<String>, override val radioMessagesOnPickup: ImmutableList<String>,
override val fuelAmount: Long?, override val fuelAmount: Long?,
@JsonPropertyConfig(isFlat = true)
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
val json: Map<String, Any> val json: Map<String, Any>

View File

@ -1,5 +1,11 @@
package ru.dbotthepony.kstarbound.io.json.builder 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 private fun Int.toBool() = if (this == 0) null else this > 0
/** /**
@ -60,6 +66,14 @@ val JsonPropertyConfig.realMustBePresent get() = mustBePresent.toBool()
/** /**
* Указывает, что для данного класса можно автоматически создать [FactoryAdapter] * Указывает, что для данного класса можно автоматически создать [FactoryAdapter]
*
* В подавляющем большинстве случаев это работает исключительно с data классами
*
* С технической точки зрения, у класса определяются все свойства и происходит поиск *главного* конструктора, а
* затем аргументы конструктора отражаются на свойства класса
*
* @see JsonIgnoreProperty
* @see JsonPropertyConfig
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@ -80,3 +94,34 @@ annotation class JsonFactory(
*/ */
val asList: Boolean = false 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 <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val delegate = type.rawType.getAnnotation(JsonImplementation::class.java)
if (delegate != null) {
return gson.getAdapter(delegate.implementingClass.java) as TypeAdapter<T>?
}
return null
}
}

View File

@ -33,8 +33,12 @@ import java.lang.reflect.Constructor
import kotlin.jvm.internal.DefaultConstructorMarker import kotlin.jvm.internal.DefaultConstructorMarker
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.reflect.* import kotlin.reflect.*
import kotlin.reflect.full.declaredMembers
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.isSupertypeOf import kotlin.reflect.full.isSupertypeOf
import kotlin.reflect.full.primaryConstructor
/** /**
* [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе. * [TypeAdapter] для классов, которые имеют все свои свойства в главном конструкторе.
@ -452,8 +456,8 @@ class FactoryAdapter<T : Any> private constructor(
* *
* На самом деле, JSON "заворачивается" в [ImmutableMap], или [ImmutableList] если указано [asList]/[inputAsList] * На самом деле, JSON "заворачивается" в [ImmutableMap], или [ImmutableList] если указано [asList]/[inputAsList]
* *
* Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] первым аргументом, * Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] последним аргументом,
* иначе поиск конструктора завершится неудчаей * иначе поиск конструктора завершится неудачей
*/ */
fun storesJson(flag: Boolean = true): Builder<T> { fun storesJson(flag: Boolean = true): Builder<T> {
storesJson = flag storesJson = flag
@ -524,9 +528,53 @@ class FactoryAdapter<T : Any> private constructor(
} }
} }
companion object { companion object : TypeAdapterFactory {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
var currentSymbolicName by ThreadLocal<String>() var currentSymbolicName by ThreadLocal<String>()
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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<T>
val builder = Builder(kclass)
val properties = kclass.declaredMembers.filterIsInstance<KProperty1<T, *>>()
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
}
} }
} }