mirror of
https://github.com/RDCN-Community-Developers/rdkt.git
synced 2026-09-05 01:35:31 +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