Skip to content

Commit

Permalink
Make SerialModule sealed class instead of interface
Browse files Browse the repository at this point in the history
    * As explained in #697, it appears that concatenating two modules as "behaviours" (as opposed to "bag of serializers") is way too dangerous for the end users and implies a lot of inconsistent behaviours and performance hits. So the most rational solution here is to prohibit such concatenations by closing the way to implement your own SerialModule
    * In order to still solve "fallback serializer" use-case, new 'default' DSL is introduced along with its support in PolymorphicSerializer

Fixes #697
Fixes #448
  • Loading branch information
qwwdfsad committed Mar 2, 2020
1 parent df8d296 commit e6d4a14
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 146 deletions.
106 changes: 53 additions & 53 deletions gradle/native_mpp.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,57 +10,57 @@ project.ext.nativeMainSets = []
project.ext.nativeTestSets = []

kotlin {
targets.metaClass.addTarget = { preset ->
def target = delegate.fromPreset(preset, preset.name)
project.ext.nativeMainSets.add(target.compilations['main'].kotlinSourceSets.first())
project.ext.nativeTestSets.add(target.compilations['test'].kotlinSourceSets.first())
}

targets {
if (project.ext.singleTargetMode) {
fromPreset(project.ext.ideaPreset, 'native')
} else {
// Linux
addTarget(presets.linuxX64)
addTarget(presets.linuxArm32Hfp)
addTarget(presets.linuxArm64)

// Mac & iOS
addTarget(presets.macosX64)

addTarget(presets.iosArm64)
addTarget(presets.iosArm32)
addTarget(presets.iosX64)

addTarget(presets.watchosX86)
addTarget(presets.watchosArm32)
addTarget(presets.watchosArm64)

addTarget(presets.tvosArm64)
addTarget(presets.tvosX64)

// Windows
addTarget(presets.mingwX64)
addTarget(presets.mingwX86)

// WASM
addTarget(presets.wasm32)
}
}

sourceSets {
nativeMain { dependsOn commonMain }
// Empty source set is required in order to have native tests task
nativeTest {}

if (!project.ext.singleTargetMode) {
configure(project.ext.nativeMainSets) {
dependsOn nativeMain
}

configure(project.ext.nativeTestSets) {
dependsOn nativeTest
}
}
}
// targets.metaClass.addTarget = { preset ->
// def target = delegate.fromPreset(preset, preset.name)
// project.ext.nativeMainSets.add(target.compilations['main'].kotlinSourceSets.first())
// project.ext.nativeTestSets.add(target.compilations['test'].kotlinSourceSets.first())
// }
//
// targets {
// if (project.ext.singleTargetMode) {
// fromPreset(project.ext.ideaPreset, 'native')
// } else {
// // Linux
// addTarget(presets.linuxX64)
// addTarget(presets.linuxArm32Hfp)
// addTarget(presets.linuxArm64)
//
// // Mac & iOS
// addTarget(presets.macosX64)
//
// addTarget(presets.iosArm64)
// addTarget(presets.iosArm32)
// addTarget(presets.iosX64)
//
// addTarget(presets.watchosX86)
// addTarget(presets.watchosArm32)
// addTarget(presets.watchosArm64)
//
// addTarget(presets.tvosArm64)
// addTarget(presets.tvosX64)
//
// // Windows
// addTarget(presets.mingwX64)
// addTarget(presets.mingwX86)
//
// // WASM
// addTarget(presets.wasm32)
// }
// }
//
// sourceSets {
// nativeMain { dependsOn commonMain }
// // Empty source set is required in order to have native tests task
// nativeTest {}
//
// if (!project.ext.singleTargetMode) {
// configure(project.ext.nativeMainSets) {
// dependsOn nativeMain
// }
//
// configure(project.ext.nativeTestSets) {
// dependsOn nativeTest
// }
// }
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public class SealedClassSerializer<T : Any>(
}.mapValues { it.value.value }
}

override fun findPolymorphicSerializer(decoder: CompositeDecoder, klassName: String): KSerializer<out T> {
override fun findPolymorphicSerializer(decoder: CompositeDecoder, klassName: String): DeserializationStrategy<out T> {
return serialName2Serializer[klassName] ?: super.findPolymorphicSerializer(decoder, klassName)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ public abstract class AbstractPolymorphicSerializer<T : Any> internal constructo
public open fun findPolymorphicSerializer(
decoder: CompositeDecoder,
klassName: String
): KSerializer<out T> = decoder.context.getPolymorphic(baseClass, klassName)
): DeserializationStrategy<out T> = decoder.context.getPolymorphic(baseClass, klassName)
?: decoder.context.getDefaultPolymorphic(baseClass, klassName)
?: throwSubtypeNotRegistered(klassName, baseClass)


Expand Down
4 changes: 4 additions & 0 deletions runtime/commonMain/src/kotlinx/serialization/internal/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,7 @@ internal fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
@PublishedApi
internal inline fun <T> KSerializer<*>.cast(): KSerializer<T> = this as KSerializer<T>

@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
@PublishedApi
internal inline fun <T> DeserializationStrategy<*>.cast(): DeserializationStrategy<T> = this as DeserializationStrategy<T>
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,18 @@ public abstract class JsonTransformingSerializer<T : Any>(

final override fun deserialize(decoder: Decoder): T {
val input = decoder.asJsonInput()
var element = input.decodeJson()
element = readTransform(element)
return input.json.fromJson(tSerializer, element)
val element = input.decodeJson()
return input.json.fromJson(tSerializer, readTransform(element))
}

/**
* Transformation which happens during [serialize] call.
* Transformation that happens during [serialize] call.
* Does nothing by default.
*/
protected open fun readTransform(element: JsonElement): JsonElement = element

/**
* Transformation which happens during [deserialize] call.
* Transformation that happens during [deserialize] call.
* Does nothing by default.
*/
protected open fun writeTransform(element: JsonElement): JsonElement = element
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ internal class ContextValidator(private val discriminator: String) : SerialModul
}
}
}

override fun <Base : Any> defaultPolymorphic(
baseClass: KClass<Base>,
defaultSerializerProvider: (className: String) -> DeserializationStrategy<out Base>?
) {
// Nothing here
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package kotlinx.serialization.modules

import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.json.*
import kotlin.reflect.*

/**
Expand All @@ -20,6 +21,7 @@ public class PolymorphicModuleBuilder<Base : Any> internal constructor(
private val baseSerializer: KSerializer<Base>? = null
) {
private val subclasses: MutableList<Pair<KClass<out Base>, KSerializer<out Base>>> = mutableListOf()
private var defaultSerializerProvider: ((String) -> DeserializationStrategy<out Base>)? = null

/**
* Adds a [subclass] [serializer] to the resulting module under the initial [baseClass].
Expand Down Expand Up @@ -59,6 +61,21 @@ public class PolymorphicModuleBuilder<Base : Any> internal constructor(
@ImplicitReflectionSerializer
public inline fun <reified T : Base> subclass(): Unit = addSubclass(T::class, serializer())

/**
* Registers serializer provider that will be invoked if no polymorphic serializer is present.
* [defaultSerializerProvider] can be stateful and lookup a serializer for the missing type dynamically.
*
* Typically, if the class is not registered in advance, it is not possible to know the structure of the unknown
* type and have a precise serializer, so the default serializer has limited capabilities.
* To have a structural access to the unknown data, it is recommended to use [JsonTransformingSerializer] subclass.
*/
public fun default(defaultSerializerProvider: (className: String) -> DeserializationStrategy<out Base>?) {
require(this.defaultSerializerProvider == null) {
"Default serializer provider is already registered for class $baseClass: ${this.defaultSerializerProvider}"
}
this.defaultSerializerProvider = defaultSerializerProvider
}

/**
* @see addSubclass
*/
Expand All @@ -74,6 +91,10 @@ public class PolymorphicModuleBuilder<Base : Any> internal constructor(
serializer.cast()
)
}

if (defaultSerializerProvider != null) {
builder.registerDefaultPolymorphicSerializer(baseClass, defaultSerializerProvider!!, false)
}
}

/**
Expand Down
106 changes: 93 additions & 13 deletions runtime/commonMain/src/kotlinx/serialization/modules/SerialModule.kt
Original file line number Diff line number Diff line change
@@ -1,60 +1,140 @@
/*
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

@file:Suppress("UNCHECKED_CAST", "RedundantVisibilityModifier")

package kotlinx.serialization.modules

import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlin.reflect.*

/**
* [SerialModule] is a collection of serializers used by [ContextSerializer] and [PolymorphicSerializer]
* to override or provide serializers at the runtime, whereas at the compile-time they provided by the serialization plugin.
* It can be considered as a map where serializers can be found using their statically known KClasses.
*
* It can be considered as a map where serializers are found using statically known KClasses.
*
* To enable runtime resolution of serializers, one of the special annotations must be used on target types
* and a serial module with serializers should be used during construction of [SerialFormat].
* To enable runtime serializers resolution, one of the special annotations must be used on target types
* ([Polymorphic] or [ContextualSerialization]), and a serial module with serializers should be used during construction of [SerialFormat].
*
* @see ContextualSerialization
* @see Polymorphic
*/
public interface SerialModule {
public sealed class SerialModule {

/**
* Returns a contextual serializer associated with a given [kclass].
* This method is used in context-sensitive operations on a property marked with [ContextualSerialization] by a [ContextSerializer]
*/
public fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>?
public abstract fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>?

/**
* Returns a polymorphic serializer registered for a class of the given [value] in the scope of [baseClass].
*/
public fun <T : Any> getPolymorphic(baseClass: KClass<T>, value: T): KSerializer<out T>?
public abstract fun <T : Any> getPolymorphic(baseClass: KClass<T>, value: T): KSerializer<out T>?

/**
* Returns a polymorphic serializer registered for for a [serializedClassName] in the scope of [baseClass].
*/
public fun <T : Any> getPolymorphic(baseClass: KClass<T>, serializedClassName: String): KSerializer<out T>?
public abstract fun <T : Any> getPolymorphic(baseClass: KClass<T>, serializedClassName: String): KSerializer<out T>?

/**
* Returns a default polymorphic serializer registered for a [serializedClassName] in the scope of [baseClass].
* In contrast with [getPolymorphic], module user registers a factory function for default serializers that can
* provide a serializer even for an unknown in advance [serializedClassName].
*/
public abstract fun <T : Any> getDefaultPolymorphic(
baseClass: KClass<T>,
serializedClassName: String
): DeserializationStrategy<out T>?

/**
* Copies contents of this module to the given [collector].
*/
public fun dumpTo(collector: SerialModuleCollector)
public abstract fun dumpTo(collector: SerialModuleCollector)
}

/**
* A [SerialModule] which is empty and always returns `null`.
*/
public object EmptyModule : SerialModule {
public object EmptyModule : SerialModule() {
public override fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>? = null
public override fun <T : Any> getPolymorphic(baseClass: KClass<T>, value: T): KSerializer<out T>? = null
public override fun <T : Any> getPolymorphic(
baseClass: KClass<T>,
serializedClassName: String
): KSerializer<out T>? = null

override fun <T : Any> getDefaultPolymorphic(
baseClass: KClass<T>,
serializedClassName: String
): DeserializationStrategy<out T>? = null

public override fun dumpTo(collector: SerialModuleCollector) = Unit
}

internal typealias PolymorphicProvider<Base> = (className: String) -> DeserializationStrategy<out Base>?

/**
* A default implementation of [SerialModule]
* which uses hash maps to store serializers associated with KClasses.
*/
@Suppress("UNCHECKED_CAST")
internal class SerialModuleImpl(
private val class2Serializer: Map<KClass<*>, KSerializer<*>>,
private val polyBase2Serializers: Map<KClass<*>, Map<KClass<*>, KSerializer<*>>>,
private val polyBase2NamedSerializers: Map<KClass<*>, Map<String, KSerializer<*>>>,
private val polyBase2DefaultProvider: Map<KClass<*>, PolymorphicProvider<*>>
) : SerialModule() {

override fun <T : Any> getPolymorphic(baseClass: KClass<T>, value: T): KSerializer<out T>? {
if (!value.isInstanceOf(baseClass)) return null
val custom = polyBase2Serializers[baseClass]?.get(value::class) as? KSerializer<out T>
if (custom != null) return custom
if (baseClass == Any::class) {
val serializer = StandardSubtypesOfAny.getSubclassSerializer(value)
return serializer as? KSerializer<out T>
}
return null
}

override fun <T : Any> getDefaultPolymorphic(
baseClass: KClass<T>,
serializedClassName: String
): DeserializationStrategy<out T>? =
(polyBase2DefaultProvider[baseClass] as? PolymorphicProvider<T>)?.invoke(serializedClassName)

override fun <T : Any> getPolymorphic(baseClass: KClass<T>, serializedClassName: String): KSerializer<out T>? {
val standardPolymorphic =
if (baseClass == Any::class) StandardSubtypesOfAny.getDefaultDeserializer(serializedClassName)
else null

if (standardPolymorphic != null) return standardPolymorphic as KSerializer<out T>
return polyBase2NamedSerializers[baseClass]?.get(serializedClassName) as? KSerializer<out T>
}

override fun <T : Any> getContextual(kclass: KClass<T>): KSerializer<T>? =
class2Serializer[kclass] as? KSerializer<T>

override fun dumpTo(collector: SerialModuleCollector) {
class2Serializer.forEach { (kclass, serial) ->
collector.contextual(
kclass as KClass<Any>,
serial.cast()
)
}

polyBase2Serializers.forEach { (baseClass, classMap) ->
classMap.forEach { (actualClass, serializer) ->
collector.polymorphic(
baseClass as KClass<Any>,
actualClass as KClass<Any>,
serializer.cast()
)
}
}

polyBase2DefaultProvider.forEach { (baseClass, provider) ->
collector.defaultPolymorphic(baseClass as KClass<Any>, provider as (PolymorphicProvider<out Any>))
}
}
}
Loading

0 comments on commit e6d4a14

Please sign in to comment.