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

Support primitives (ints, strings, JsonLiterals, etc) on a top-level #465

Closed
wants to merge 2 commits into from
Closed
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
16 changes: 2 additions & 14 deletions runtime/commonMain/src/kotlinx/serialization/Tagged.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
/*
* Copyright 2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization
Expand Down Expand Up @@ -149,7 +137,7 @@ abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {
protected val currentTagOrNull
get() = tagStack.lastOrNull()

private fun pushTag(name: Tag) {
protected fun pushTag(name: Tag) {
tagStack.add(name)
}

Expand Down
14 changes: 1 addition & 13 deletions runtime/commonMain/src/kotlinx/serialization/internal/Enums.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
/*
* Copyright 2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.internal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.json
Expand Down Expand Up @@ -28,9 +28,9 @@ public object JsonElementSerializer : KSerializer<JsonElement> {
override fun serialize(encoder: Encoder, obj: JsonElement) {
verify(encoder)
when (obj) {
is JsonPrimitive -> JsonPrimitiveSerializer.serialize(encoder, obj)
is JsonObject -> JsonObjectSerializer.serialize(encoder, obj)
is JsonArray -> JsonArraySerializer.serialize(encoder, obj)
is JsonPrimitive -> encoder.encodeSerializableValue(JsonPrimitiveSerializer, obj)
is JsonObject -> encoder.encodeSerializableValue(JsonObjectSerializer, obj)
is JsonArray -> encoder.encodeSerializableValue(JsonArraySerializer, obj)
}
}

Expand All @@ -52,16 +52,16 @@ public object JsonPrimitiveSerializer : KSerializer<JsonPrimitive> {
override fun serialize(encoder: Encoder, obj: JsonPrimitive) {
verify(encoder)
return if (obj is JsonNull) {
JsonNullSerializer.serialize(encoder, JsonNull)
encoder.encodeSerializableValue(JsonNullSerializer, JsonNull)
} else {
JsonLiteralSerializer.serialize(encoder, obj as JsonLiteral)
encoder.encodeSerializableValue(JsonLiteralSerializer, obj as JsonLiteral)
}
}

override fun deserialize(decoder: Decoder): JsonPrimitive {
verify(decoder)
return if (decoder.decodeNotNullMark()) JsonPrimitive(decoder.decodeString())
else JsonNullSerializer.deserialize(decoder)
else decoder.decodeSerializableValue(JsonNullSerializer)
}

private object JsonPrimitiveDescriptor : SerialClassDescImpl("JsonPrimitive") {
Expand Down Expand Up @@ -90,7 +90,8 @@ public object JsonNullSerializer : KSerializer<JsonNull> {
}

private object JsonNullDescriptor : SerialClassDescImpl("JsonNull") {
override val kind: SerialKind get() = UnionKind.OBJECT
// technically, JsonNull is an object, but it does not call beginStructure/endStructure
override val kind: SerialKind get() = UnionKind.ENUM_KIND
}
}

Expand Down Expand Up @@ -185,4 +186,4 @@ private fun verify(encoder: Encoder) {

private fun verify(decoder: Decoder) {
if (decoder !is JsonInput) error("Json element serializer can be used only by Json format")
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
/*
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

@file:Suppress("LeakingThis")

package kotlinx.serialization.json.internal

import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.internal.EnumDescriptor
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
import kotlin.jvm.*
import kotlinx.serialization.modules.SerialModule
import kotlin.jvm.JvmField

internal fun <T> Json.readJson(element: JsonElement, deserializer: DeserializationStrategy<T>): T {
val descriptor = deserializer.descriptor
if (element is JsonNull) {
// TODO temporary workaround (?)
require(descriptor.isNullable) { "Read JsonNull and expected nullable descriptor, but has $descriptor" }
@Suppress("NULL_FOR_NONNULL_TYPE")
return null
}

val input = when (descriptor.kind) {
StructureKind.LIST -> JsonTreeListInput(this, cast(element))
StructureKind.MAP -> JsonTreeMapInput(this, cast(element))
is PrimitiveKind -> JsonPrimitiveInput(this, cast(element))
else -> JsonTreeInput(this, cast(element))
val input = when (element) {
is JsonObject -> JsonTreeInput(this, element)
is JsonArray -> JsonTreeListInput(this, element)
is JsonLiteral, JsonNull -> JsonPrimitiveInput(this, element as JsonPrimitive)
}

return input.decode(deserializer)
Expand Down Expand Up @@ -94,16 +89,12 @@ private sealed class AbstractJsonTreeInput(override val json: Json, open val obj

private class JsonPrimitiveInput(json: Json, override val obj: JsonPrimitive) : AbstractJsonTreeInput(json, obj) {

companion object {
const val primitive = "primitive"
}

init {
pushTag(primitive)
pushTag(PRIMITIVE_TAG)
}

override fun currentElement(tag: String): JsonElement {
require(tag == primitive)
require(tag === PRIMITIVE_TAG) { "This input can only handle primitives with '$PRIMITIVE_TAG' tag" }
return obj
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
/*
* Copyright 2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.json.internal

import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.internal.EnumDescriptor
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
import kotlinx.serialization.modules.SerialModule
import kotlin.collections.set
import kotlin.jvm.*
import kotlin.jvm.JvmField

internal fun <T> Json.writeJson(value: T, serializer: SerializationStrategy<T>): JsonElement {
lateinit var result: JsonElement
Expand Down Expand Up @@ -68,8 +56,12 @@ private sealed class AbstractJsonTreeOutput(
}

override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
encodePolymorphically(serializer, value) {
writePolymorphic = true
// Writing non-structured data (i.e. primitives) on top-level (e.g. without any tag) requires special output
if (currentTagOrNull != null || serializer.descriptor.kind !is PrimitiveKind && serializer.descriptor.kind !== UnionKind.ENUM_KIND) {
encodePolymorphically(serializer, value) { writePolymorphic = true }
} else JsonPrimitiveOutput(json, nodeConsumer).apply {
encodeSerializableValue(serializer, value)
endEncode(serializer.descriptor)
}
}

Expand Down Expand Up @@ -118,6 +110,26 @@ private sealed class AbstractJsonTreeOutput(
}
}

internal const val PRIMITIVE_TAG = "primitive" // also used in JsonPrimitiveInput

private class JsonPrimitiveOutput(json: Json, nodeConsumer: (JsonElement) -> Unit) :
AbstractJsonTreeOutput(json, nodeConsumer) {
private var content: JsonElement? = null

init {
pushTag(PRIMITIVE_TAG)
}

override fun putElement(key: String, element: JsonElement) {
require(key === PRIMITIVE_TAG) { "This output can only consume primitives with '$PRIMITIVE_TAG' tag" }
require(content == null) { "Primitive element was already recorded. Does call to .encodeXxx happen more than once?" }
content = element
}

override fun getCurrent(): JsonElement =
requireNotNull(content) { "Primitive element has not been recorded. Is call to .encodeXxx is missing in serializer?" }
}

private open class JsonTreeOutput(json: Json, nodeConsumer: (JsonElement) -> Unit) :
AbstractJsonTreeOutput(json, nodeConsumer) {

Expand Down
16 changes: 3 additions & 13 deletions runtime/commonTest/src/kotlinx/serialization/SampleClasses.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
/*
* Copyright 2019 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization
Expand All @@ -21,3 +9,5 @@ data class IntData(val intV: Int)

@Serializable
data class StringData(val data: String)

enum class SampleEnum { OptionA, OptionB, OptionC }
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package kotlinx.serialization.features

import kotlinx.serialization.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.test.CommonEnumSerializer
import kotlinx.serialization.test.isJvm
import kotlin.test.*

Expand Down Expand Up @@ -51,8 +52,6 @@ data class DataZooIsomorphic(
val mm: Map<String, Data2>? = null
)

private enum class SampleEnum { OptionA, OptionB, OptionC }

@Serializable
private data class DataWithEnum(val s: String, val enum: SampleEnum, val enumList: List<SampleEnum> = emptyList())

Expand Down Expand Up @@ -118,12 +117,8 @@ class SchemaTest {
fun enumDescriptors() {
val dataDescriptor = DataWithEnum.serializer().descriptor
val enumDesc = dataDescriptor.getElementDescriptor(1)
val serialName = if (isJvm()) "kotlinx.serialization.features.SampleEnum" else "SampleEnum"
val manualSerializer = CommonEnumSerializer(
serialName,
enumValues<SampleEnum>(),
enumValues<SampleEnum>().map { it.name }.toTypedArray()
)
val serialName = if (isJvm()) "kotlinx.serialization.SampleEnum" else "SampleEnum"
val manualSerializer = CommonEnumSerializer<SampleEnum>(serialName)

assertEquals(enumDesc, manualSerializer.descriptor)
assertEquals(enumDesc, dataDescriptor.getElementDescriptor(2).getElementDescriptor(0))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ abstract class JsonTestBase {
assertEquals(streamingResult.getOrNull()!!, treeResult.getOrNull()!!)
}

internal fun <T: Any> parametrizedTest(serializer: KSerializer<T>, data: T, expected: String, json: Json = unquoted) {
internal fun <T> parametrizedTest(serializer: KSerializer<T>, data: T, expected: String, json: Json = unquoted) {
parametrizedTest { useStreaming ->
val serialized = json.stringify(serializer, data, useStreaming)
assertEquals(expected, serialized)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.json.serializers

import kotlinx.serialization.SampleEnum
import kotlinx.serialization.internal.*
import kotlinx.serialization.json.JsonTestBase
import kotlinx.serialization.test.CommonEnumSerializer
import kotlin.test.Test

class JsonNativePrimitivesTest : JsonTestBase() {
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
@Test
fun testTopLevelNativeInt() = parametrizedTest(IntSerializer, 42, "42", strict)

@Test
fun testTopLevelNativeString() = parametrizedTest(StringSerializer, "42", "\"42\"", strict)

@Test
fun testTopLevelNativeChar() = parametrizedTest(CharSerializer, '4', "\"4\"", strict)

@Test
fun testTopLevelNativeBoolean() = parametrizedTest(BooleanSerializer, true, "true", strict)

@Test
fun testTopLevelNativeEnum() =
parametrizedTest(CommonEnumSerializer("SampleEnum"), SampleEnum.OptionB, "\"OptionB\"", strict)

@Test
fun testTopLevelNativeNullable() = parametrizedTest(NullableSerializer(IntSerializer), null, "null", strict)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ class JsonNullSerializerTest : JsonTestBase() {
assertStringFormAndRestored("{\"primitive\":null}", JsonPrimitiveWrapper(JsonNull), JsonPrimitiveWrapper.serializer())
}

@Test // TODO Top-level nulls are not supported in tagged encoders
fun testTopLevelJsonNull() { // parametrizedTest { useStreaming ->
val string = strict.stringify(JsonNullSerializer, JsonNull)
@Test
fun testTopLevelJsonNull() = parametrizedTest { useStreaming ->
val string = strict.stringify(JsonNullSerializer, JsonNull, useStreaming)
assertEquals("null", string)
assertEquals(JsonNull, strict.parse(JsonNullSerializer, string))
assertEquals(JsonNull, strict.parse(JsonNullSerializer, string, useStreaming))
}

@Test // TODO Top-level nulls are not supported in tagged encoders
fun testTopLevelJsonNullAsElement() { // parametrizedTest { useStreaming ->
val string = strict.stringify(JsonElementSerializer, JsonNull)
@Test
fun testTopLevelJsonNullAsElement() = parametrizedTest { useStreaming ->
val string = strict.stringify(JsonElementSerializer, JsonNull, useStreaming)
assertEquals("null", string)
assertEquals(JsonNull, strict.parse(JsonElementSerializer, string))
assertEquals(JsonNull, strict.parse(JsonElementSerializer, string, useStreaming))
}

@Test // TODO Top-level nulls are not supported in tagged encoders
fun testTopLevelJsonNullAsPrimitive() { // parametrizedTest { useStreaming ->
val string = strict.stringify(JsonPrimitiveSerializer, JsonNull)
@Test
fun testTopLevelJsonNullAsPrimitive() = parametrizedTest { useStreaming ->
val string = strict.stringify(JsonPrimitiveSerializer, JsonNull, useStreaming)
assertEquals("null", string)
assertEquals(JsonNull, strict.parse(JsonPrimitiveSerializer, string))
assertEquals(JsonNull, strict.parse(JsonPrimitiveSerializer, string, useStreaming))
}

@Test
Expand Down
Loading