aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/utils/KSerializable.kt
blob: ad90ff863ba9106d0eb1d83107795dc8bff53c1c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package at.hannibal2.skyhanni.utils

import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.annotations.SerializedName
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.javaType
import kotlin.reflect.typeOf
import com.google.gson.internal.`$Gson$Types` as InternalGsonTypes

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class KSerializable

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ExtraData

class KotlinTypeAdapterFactory : TypeAdapterFactory {

    internal data class ParameterInfo(
        val param: KParameter,
        val adapter: TypeAdapter<Any?>,
        val name: String,
        val field: KProperty1<Any, Any?>,
    )

    @OptIn(ExperimentalStdlibApi::class)
    override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
        val kotlinClass = type.rawType.kotlin as KClass<T>
        if (kotlinClass.findAnnotation<KSerializable>() == null) return null
        if (!kotlinClass.isData) return null
        val primaryConstructor = kotlinClass.primaryConstructor ?: return null
        val params = primaryConstructor.parameters.filter { it.findAnnotation<ExtraData>() == null }
        val extraDataParam = primaryConstructor.parameters
            .find { it.findAnnotation<ExtraData>() != null && typeOf<MutableMap<String, JsonElement>>().isSubtypeOf(it.type) }
            ?.let { param ->
                param to kotlinClass.memberProperties.find { it.name == param.name && it.returnType.isSubtypeOf(typeOf<Map<String, JsonElement>>()) } as KProperty1<Any, Map<String, JsonElement>>
            }
        val parameterInfos = params.map { param ->
            ParameterInfo(
                param,
                gson.getAdapter(
                    TypeToken.get(InternalGsonTypes.resolve(type.type, type.rawType, param.type.javaType))
                ) as TypeAdapter<Any?>,
                param.findAnnotation<SerializedName>()?.value ?: param.name!!,
                kotlinClass.memberProperties.find { it.name == param.name }!! as KProperty1<Any, Any?>
            )
        }.associateBy { it.name }
        val jsonElementAdapter = gson.getAdapter(JsonElement::class.java)

        return object : TypeAdapter<T>() {
            override fun write(out: JsonWriter, value: T?) {
                if (value == null) {
                    out.nullValue()
                    return
                }
                out.beginObject()
                for ((name, paramInfo) in parameterInfos) {
                    out.name(name)
                    paramInfo.adapter.write(out, paramInfo.field.get(value))
                }
                if (extraDataParam != null) {
                    val extraData = extraDataParam.second.get(value)
                    for ((extraName, extraValue) in extraData) {
                        out.name(extraName)
                        jsonElementAdapter.write(out, extraValue)
                    }
                }
                out.endObject()
            }

            override fun read(reader: JsonReader): T? {
                if (reader.peek() == JsonToken.NULL) {
                    reader.nextNull()
                    return null
                }
                reader.beginObject()
                val args = mutableMapOf<KParameter, Any?>()
                val extraData = mutableMapOf<String, JsonElement>()
                while (reader.peek() != JsonToken.END_OBJECT) {
                    val name = reader.nextName()
                    val paramData = parameterInfos[name]
                    if (paramData == null) {
                        extraData[name] = jsonElementAdapter.read(reader)
                        continue
                    }
                    val value = paramData.adapter.read(reader)
                    args[paramData.param] = value
                }
                reader.endObject()
                if (extraDataParam != null) {
                    args[extraDataParam.first] = extraData
                }
                return primaryConstructor.callBy(args)
            }
        }
    }
}