Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify Dynamic parser/serializer with the rest of JSON #895

Merged
merged 6 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions runtime/api/kotlinx-serialization-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ public final class kotlinx/serialization/MigrationsKt {
public static final fun load (Lkotlinx/serialization/BinaryFormat;[B)Ljava/lang/Object;
public static final fun loads (Lkotlinx/serialization/BinaryFormat;Ljava/lang/String;)Ljava/lang/Object;
public static final fun loads (Lkotlinx/serialization/BinaryFormat;Lkotlinx/serialization/DeserializationStrategy;Ljava/lang/String;)Ljava/lang/Object;
public static final fun noImpl ()Ljava/lang/Void;
public static final fun parse (Lkotlinx/serialization/StringFormat;Ljava/lang/String;)Ljava/lang/Object;
public static final fun parse (Lkotlinx/serialization/StringFormat;Lkotlinx/serialization/DeserializationStrategy;Ljava/lang/String;)Ljava/lang/Object;
public static final fun stringify (Lkotlinx/serialization/StringFormat;Ljava/lang/Object;)Ljava/lang/String;
Expand Down
3 changes: 2 additions & 1 deletion runtime/commonMain/src/kotlinx/serialization/Migrations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import kotlinx.serialization.modules.*
import kotlin.internal.*
import kotlin.reflect.*

private fun noImpl(): Nothing = throw UnsupportedOperationException("Not implemented, should not be called")
@PublishedApi
internal fun noImpl(): Nothing = throw UnsupportedOperationException("Not implemented, should not be called")

@Deprecated(
message = "Deprecated in the favour of PrimitiveDescriptor factory function",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ private fun noImpl(): Nothing = throw UnsupportedOperationException("Not impleme

@Deprecated(
level = DeprecationLevel.ERROR,
message = "SerialModule was renamed to SerializersModule during serialization 1.0 API stabilization"
message = "SerialModule was renamed to SerializersModule during serialization 1.0 API stabilization",
replaceWith = ReplaceWith("SerializersModule")
)
public typealias SerialModule = SerializersModule

Expand Down
76 changes: 76 additions & 0 deletions runtime/jsMain/src/kotlinx/serialization/Dynamics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization

import kotlinx.serialization.builtins.LongAsStringSerializer
import kotlinx.serialization.internal.DynamicObjectParser
import kotlinx.serialization.internal.DynamicObjectSerializer
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*

/**
* Converts native JavaScript objects into Kotlin ones, verifying their types.
*
* A result of `decodeFromDynamic(nativeObj)` should be the same as
* `kotlinx.serialization.json.Json.decodeFromString(kotlin.js.JSON.stringify(nativeObj))`.
* This class also supports array-based polymorphism if the corresponding flag in [Json.configuration] is set to `true`.
* Does not support any other [Map] keys than [String].
* Has limitation on [Long] type: any JS number that is greater than
* [`abs(2^53-1)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* is considered to be imprecise and therefore can't be deserialized to [Long]. Either use [Double] type
* for such values or pass them as strings using [LongAsStringSerializer] afterwards.
*
* Usage example:
*
* ```
* @Serializable
* data class Data(val a: Int)
*
* @Serializable
* data class DataWrapper(val s: String, val d: Data?)
*
* val dyn: dynamic = js("""{s:"foo", d:{a:42}}""")
* val parsed = Json.decodeFromDynamic(dyn, DataWrapper.serializer())
* parsed == DataWrapper("foo", Data(42)) // true
* ```
*/
public fun <T> Json.decodeFromDynamic(deserializer: DeserializationStrategy<T>, dynamic: dynamic): T {
return DynamicObjectParser(serializersModule, configuration).parse(dynamic, deserializer)
}

/**
* A reified version of [decodeFromDynamic].
*/
public inline fun <reified T> Json.decodeFromDynamic(dynamic: dynamic): T =
decodeFromDynamic(serializersModule.getContextualOrDefault(), dynamic)

/**
* Converts Kotlin data structures to plain Javascript objects
*
* Limitations:
* * Map keys must be of primitive or enum type
* * Currently does not support polymorphism
* * All [Long] values must be less than [`abs(2^53-1)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
* Otherwise, they're encoded as doubles with precision loss and require `isLenient` flag of [Json.configuration] set to true.
*
* Example of usage:
* ```
* @Serializable
* open class DataWrapper(open val s: String, val d: String?)
*
* val wrapper = DataWrapper("foo", "bar")
* val plainJS: dynamic = Json.encodeToDynamic(DataWrapper.serializer(), wrapper)
* ```
*
*/
public fun <T> Json.encodeToDynamic(serializer: SerializationStrategy<T>, value: T): dynamic {
return DynamicObjectSerializer(serializersModule, configuration, false).serialize(serializer, value)
}

/**
* A reified version of [encodeToDynamic].
*/
public inline fun <reified T : Any> Json.encodeToDynamic(value: T): dynamic =
encodeToDynamic(serializersModule.getContextualOrDefault(), value)
49 changes: 49 additions & 0 deletions runtime/jsMain/src/kotlinx/serialization/DynamicsMigration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization

import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*

private const val parserMessage = "DynamicObjectParser and its 'parse' method were unified with Json operations. " +
"Please use Json's 'decodeFromDynamic' extension."

// ReplaceWith/typealias is missing intentionally because if we replace all instances of DynamicObjectParser with Json,
// we'll get incorrect `parse` deprecation (because of StringFormat.parse(serializer, string) overload)
@Deprecated(parserMessage, level = DeprecationLevel.ERROR)
public class DynamicObjectParser constructor(
public val context: SerializersModule = EmptySerializersModule,
public val configuration: JsonConfiguration = JsonConfiguration.Default
) {
@Deprecated(
parserMessage,
ReplaceWith("Json(configuration, context).decodeFromDynamic(value)", "kotlinx.serialization.json.Json"),
level = DeprecationLevel.ERROR
)
public inline fun <reified T : Any> parse(value: dynamic): T = noImpl()

@Deprecated(
parserMessage,
ReplaceWith(
"Json(configuration, context).decodeFromDynamic(deserializer, obj)",
"kotlinx.serialization.json.Json"
),
level = DeprecationLevel.ERROR
)
public fun <T> parse(obj: dynamic, deserializer: DeserializationStrategy<T>): T = noImpl()
}

private const val serializerMessage =
"DynamicObjectSerializer and its 'serialize' method were unified with Json operations. " +
"Please use Json's 'encodeToDynamic' extension."

@Deprecated(serializerMessage, ReplaceWith("Json", "kotlinx.serialization.json.Json"), level = DeprecationLevel.ERROR)
public typealias DynamicObjectSerializer = Json

@Deprecated(serializerMessage, ReplaceWith("encodeToDynamic(strategy, obj)"), level = DeprecationLevel.ERROR)
public fun <T> Json.serialize(strategy: SerializationStrategy<T>, obj: T): dynamic = noImpl()

@Deprecated(serializerMessage, ReplaceWith("encodeToDynamic<T>(obj)"), level = DeprecationLevel.ERROR)
public inline fun <reified T : Any> Json.serialize(obj: T): dynamic = noImpl()
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization
package kotlinx.serialization.internal

import kotlinx.serialization.internal.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
import kotlin.math.*
Expand All @@ -14,39 +14,10 @@ import kotlin.math.*
*/
internal const val MAX_SAFE_INTEGER: Double = 9007199254740991.toDouble() // 2^53 - 1

/**
* Converts native JavaScript objects into Kotlin ones, verifying their types.
*
* A result of `parse(nativeObj)` should be the same as
* `kotlinx.serialization.json.Json.parse(kotlin.js.JSON.stringify(nativeObj))`.
* This class also supports array-based polymorphism if the corresponding flag in [configuration] is set to `true`.
* Does not support any other [Map] keys than [String].
* Has limitation on [Long] type: any JS number that is greater than
* [`abs(2^53-1)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
* is considered to be imprecise and therefore can't be deserialized to [Long]. Either use [Double] type
* for such values or pass them as strings using [LongAsStringSerializer] afterwards.
*
* Usage example:
*
* ```
* @Serializable
* data class Data(val a: Int)
*
* @Serializable
* data class DataWrapper(val s: String, val d: Data?)
*
* val dyn: dynamic = js("""{s:"foo", d:{a:42}}""")
* val parsed = DynamicObjectParser().parse(dyn, DataWrapper.serializer())
* parsed == DataWrapper("foo", Data(42)) // true
* ```
*/
public class DynamicObjectParser @OptIn(UnstableDefault::class) constructor(
override val serializersModule: SerializersModule = EmptySerializersModule,
internal val configuration: JsonConfiguration = JsonConfiguration.Default
internal class DynamicObjectParser(
override val serializersModule: SerializersModule,
internal val configuration: JsonConfiguration
) : SerialFormat {

inline fun <reified T : Any> parse(value: dynamic): T = parse(value, serializersModule.getContextualOrDefault())

/**
* Deserializes given [obj] from dynamic form to type [T] using [deserializer].
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package kotlinx.serialization
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.internal

import kotlinx.serialization.builtins.*
import kotlinx.serialization.encoding.AbstractEncoder
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.json.internal.BEGIN_LIST
import kotlinx.serialization.json.internal.BEGIN_OBJ
import kotlinx.serialization.json.internal.END_LIST
import kotlinx.serialization.json.internal.END_OBJ
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
import kotlin.math.abs
import kotlin.math.floor
import kotlinx.serialization.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
import kotlinx.serialization.json.internal.*
import kotlinx.serialization.modules.*
import kotlin.math.*


/**
Expand All @@ -30,34 +29,30 @@ import kotlin.math.floor
* val wrapper = DataWrapper("foo", "bar")
* val plainJS: dynamic = DynamicObjectSerializer().serialize(DataWrapper.serializer(), wrapper)
* ```
*
* @param encodeNullAsUndefined if true null properties will be omitted from the output
*/
public class DynamicObjectSerializer @OptIn(UnstableDefault::class) constructor(
public val context: SerializersModule = EmptySerializersModule,
private val configuration: JsonConfiguration = JsonConfiguration.Default,
private val encodeNullAsUndefined: Boolean = false
internal class DynamicObjectSerializer(
val serializersModule: SerializersModule,
private val configuration: JsonConfiguration,
private val encodeNullAsUndefined: Boolean
) {

public fun <T> serialize(strategy: SerializationStrategy<T>, obj: T): dynamic {
if (strategy.descriptor.kind is PrimitiveKind || strategy.descriptor.kind is SerialKind.ENUM) {
val serializer = DynamicPrimitiveEncoder(configuration)
val serializer = DynamicPrimitiveEncoder(serializersModule, configuration)
serializer.encodeSerializableValue(strategy, obj)
return serializer.result
}
val serializer = DynamicObjectEncoder(configuration, encodeNullAsUndefined)
val serializer = DynamicObjectEncoder(serializersModule, configuration, encodeNullAsUndefined)
serializer.encodeSerializableValue(strategy, obj)
return serializer.result
}

public inline fun <reified T : Any> serialize(obj: T): dynamic =
serialize(serializer(), obj)

public inline fun <reified T : Any> serialize(obj: List<T?>): dynamic =
serialize(ListSerializer(serializer<T>().nullable), obj)
}

private class DynamicObjectEncoder(val configuration: JsonConfiguration, val encodeNullAsUndefined: Boolean) :
private class DynamicObjectEncoder(
override val serializersModule: SerializersModule,
val configuration: JsonConfiguration,
val encodeNullAsUndefined: Boolean
) :
AbstractEncoder() {
private object NoOutputMark

Expand Down Expand Up @@ -212,7 +207,10 @@ private class DynamicObjectEncoder(val configuration: JsonConfiguration, val enc
}
}

private class DynamicPrimitiveEncoder(private val configuration: JsonConfiguration) : AbstractEncoder() {
private class DynamicPrimitiveEncoder(
override val serializersModule: SerializersModule,
private val configuration: JsonConfiguration
) : AbstractEncoder() {
var result: dynamic = null

override fun encodeNull() {
Expand Down
Loading