mirror of
https://github.com/RDCN-Community-Developers/rdkt.git
synced 2026-09-04 17:25:33 +08:00
Added FieldFlatteningSerializer.
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package cn.rdlevel.rdkt.core.serialization
|
||||||
|
|
||||||
|
import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.SerialInfo
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonTransformingSerializer
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to indicate that a field should be flattened when serialized.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@SerialInfo
|
||||||
|
@RDKTInternalAPI
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
public annotation class Flatten
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A serializer that can handle flattening of fields annotated with [Flatten].
|
||||||
|
*
|
||||||
|
* When serializing, the fields annotated with [Flatten] will be flattened into the parent JSON object.
|
||||||
|
* When deserializing, the fields annotated with [Flatten] will be deserialized from the parent JSON object,
|
||||||
|
* with elements that do not belong to any non-flattened fields.
|
||||||
|
*
|
||||||
|
* It's recommended to annotate classes that need to be flattened with [JsonIgnoreUnknownKeys][kotlinx.serialization.json.JsonIgnoreUnknownKeys]
|
||||||
|
* when there are multiple flattened fields, so that fields that only belong to some classes can be ignored when deserializing other classes.
|
||||||
|
*
|
||||||
|
* This serializer only supports JSON encoding and decoding.
|
||||||
|
*
|
||||||
|
* @param tSerializer The serializer, usually the [generated one][kotlinx.serialization.KeepGeneratedSerializer], for the type T that this serializer will delegate to.
|
||||||
|
*/
|
||||||
|
@RDKTInternalAPI
|
||||||
|
public class FieldFlatteningSerializer<T>(
|
||||||
|
tSerializer: KSerializer<T>
|
||||||
|
) : JsonTransformingSerializer<T>(tSerializer) {
|
||||||
|
private val flattenFieldNames: MutableList<String> = mutableListOf()
|
||||||
|
private val notFlattenFieldNames: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
(0..<descriptor.elementsCount).forEach {
|
||||||
|
val name = descriptor.getElementName(it)
|
||||||
|
if (descriptor.getElementAnnotations(it).filterIsInstance<Flatten>().isEmpty()) {
|
||||||
|
notFlattenFieldNames += name
|
||||||
|
} else {
|
||||||
|
flattenFieldNames += name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transformSerialize(element: JsonElement): JsonElement {
|
||||||
|
return buildJsonObject {
|
||||||
|
element.jsonObject.forEach { (k, v) ->
|
||||||
|
if (k !in flattenFieldNames) {
|
||||||
|
put(k, v)
|
||||||
|
} else {
|
||||||
|
v.jsonObject.forEach { (k2, v2) ->
|
||||||
|
put(k2, v2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transformDeserialize(element: JsonElement): JsonElement {
|
||||||
|
return buildJsonObject base@{
|
||||||
|
val flattened = buildJsonObject flattened@{
|
||||||
|
element.jsonObject.forEach { (k, v) ->
|
||||||
|
if (k in notFlattenFieldNames) {
|
||||||
|
this@base.put(k, v)
|
||||||
|
} else {
|
||||||
|
put(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flattenFieldNames.forEach {
|
||||||
|
put(it, flattened)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [KSerializer] into a [FieldFlatteningSerializer] that can handle flattening of fields annotated with [Flatten].
|
||||||
|
*/
|
||||||
|
@RDKTInternalAPI
|
||||||
|
public fun <T> KSerializer<T>.flatten(): FieldFlatteningSerializer<T> = FieldFlatteningSerializer(this)
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package cn.rdlevel.rdkt.core.serialization
|
||||||
|
|
||||||
|
import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import kotlin.reflect.KMutableProperty1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [KSerializer] that flattens the fields of the serialized type into the same level as the base type.
|
||||||
|
*
|
||||||
|
* Fields to be flattened should be annotated with [kotlinx.serialization.Transient],
|
||||||
|
* and types to be flattened or serialized should be annotated with [JsonIgnoreUnknownKeys].
|
||||||
|
*
|
||||||
|
* This serializer only supports JSON encoding and decoding.
|
||||||
|
*
|
||||||
|
* This serializer is legacy, but I want to keep it because I think it's cool.
|
||||||
|
*
|
||||||
|
* @property tSerializer The original serializer for the base type.
|
||||||
|
*/
|
||||||
|
@RDKTInternalAPI
|
||||||
|
public class LegacyFieldFlatteningSerializer<Type>(
|
||||||
|
private val tSerializer: KSerializer<Type>,
|
||||||
|
) : KSerializer<Type> {
|
||||||
|
public class Entry<T, V>(
|
||||||
|
private val serializer: KSerializer<V>,
|
||||||
|
private val supplier: (T) -> V,
|
||||||
|
private val consumer: (T, V) -> Unit,
|
||||||
|
) {
|
||||||
|
public fun encodeEntry(builder: JsonObjectBuilder, json: Json, baseValue: T) {
|
||||||
|
json.encodeToJsonElement(serializer, supplier(baseValue)).jsonObject.forEach { (k, v) ->
|
||||||
|
builder.put(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun decodeEntry(json: Json, baseValue: T, element: JsonElement) {
|
||||||
|
consumer(baseValue, json.decodeFromJsonElement(serializer, element))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val flatteningEntries: MutableList<Entry<Type, *>> = mutableListOf()
|
||||||
|
|
||||||
|
public fun flatten(entry: Entry<Type, *>) {
|
||||||
|
flatteningEntries.add(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor get() = tSerializer.descriptor
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Type) {
|
||||||
|
val jsonEncoder = encoder as? JsonEncoder
|
||||||
|
?: error("${LegacyFieldFlatteningSerializer::class.simpleName} only supports JSON encoding.")
|
||||||
|
|
||||||
|
val element = buildJsonObject {
|
||||||
|
val json = jsonEncoder.json
|
||||||
|
json.encodeToJsonElement(tSerializer, value).jsonObject.forEach { (k, v) ->
|
||||||
|
put(k, v)
|
||||||
|
}
|
||||||
|
flatteningEntries.forEach { it.encodeEntry(this, json, value) }
|
||||||
|
}
|
||||||
|
jsonEncoder.encodeJsonElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Type {
|
||||||
|
val jsonDecoder = decoder as? JsonDecoder
|
||||||
|
?: error("${LegacyFieldFlatteningSerializer::class.simpleName} only supports JSON decoding.")
|
||||||
|
val element = jsonDecoder.decodeJsonElement()
|
||||||
|
val json = jsonDecoder.json
|
||||||
|
val value = json.decodeFromJsonElement(tSerializer, element)
|
||||||
|
flatteningEntries.forEach { it.decodeEntry(json, value, element) }
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a flattened field to the serializer.
|
||||||
|
*/
|
||||||
|
@RDKTInternalAPI
|
||||||
|
public inline fun <Ser : LegacyFieldFlatteningSerializer<T>, T, reified F> Ser.flatten(
|
||||||
|
property: KMutableProperty1<T, F>,
|
||||||
|
fieldSerializer: KSerializer<F>? = null,
|
||||||
|
): Ser = apply {
|
||||||
|
flatten(
|
||||||
|
LegacyFieldFlatteningSerializer.Entry(
|
||||||
|
fieldSerializer ?: serializer<F>(),
|
||||||
|
property::get,
|
||||||
|
property::set,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [KSerializer] to a [LegacyFieldFlatteningSerializer] and adds a flattened field to it.
|
||||||
|
*/
|
||||||
|
@RDKTInternalAPI
|
||||||
|
public inline fun <T, reified F> KSerializer<T>.flatten(
|
||||||
|
property: KMutableProperty1<T, F>,
|
||||||
|
fieldSerializer: KSerializer<F>? = null,
|
||||||
|
): LegacyFieldFlatteningSerializer<T> = LegacyFieldFlatteningSerializer(this).flatten(property, fieldSerializer)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@file:OptIn(RDKTInternalAPI::class, ExperimentalSerializationApi::class)
|
||||||
|
|
||||||
|
package cn.rdlevel.rdkt.core.test
|
||||||
|
|
||||||
|
import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI
|
||||||
|
import cn.rdlevel.rdkt.core.serialization.Flatten
|
||||||
|
import cn.rdlevel.rdkt.core.serialization.flatten
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.KeepGeneratedSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class FieldFlatteningSerializerTest {
|
||||||
|
@Serializable
|
||||||
|
data class Child(
|
||||||
|
val a: Int,
|
||||||
|
val b: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable(Parent.Serializer::class)
|
||||||
|
@KeepGeneratedSerializer
|
||||||
|
data class Parent(
|
||||||
|
val c: Double,
|
||||||
|
val d: Boolean,
|
||||||
|
@Flatten
|
||||||
|
val child: Child,
|
||||||
|
) {
|
||||||
|
object Serializer : KSerializer<Parent> by generatedSerializer().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test() {
|
||||||
|
val data = Parent(1.23, true, Child(42, "hello"))
|
||||||
|
val json = Json.encodeToString(Parent.Serializer, data)
|
||||||
|
assertEquals("""{"c":1.23,"d":true,"a":42,"b":"hello"}""", json)
|
||||||
|
val data2: Parent = Json.decodeFromString(json)
|
||||||
|
assertEquals(data2, data2)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user