diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt index 84e58399f..d061e40ac 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt @@ -123,13 +123,27 @@ internal abstract class ProtobufTaggedEncoder : ProtobufTaggedBase(), Encoder, C public final override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String): Unit = encodeTaggedString(descriptor.getTag(index), value) + private fun SerialKind.isMapOrList() = + this == StructureKind.MAP || this == StructureKind.LIST + public final override fun encodeSerializableElement( descriptor: SerialDescriptor, index: Int, serializer: SerializationStrategy, value: T ) { - nullableMode = NullableMode.NOT_NULL + nullableMode = + if (descriptor.isElementOptional(index)) + NullableMode.OPTIONAL + else { + val elementDescriptor = descriptor.getElementDescriptor(index) + if (elementDescriptor.kind.isMapOrList()) + NullableMode.COLLECTION + else if (!descriptor.kind.isMapOrList() && elementDescriptor.isNullable) // or: `serializer.descriptor` + NullableMode.ACCEPTABLE + else + NullableMode.NOT_NULL + } pushTag(descriptor.getTag(index)) encodeSerializableValue(serializer, value) @@ -141,14 +155,12 @@ internal abstract class ProtobufTaggedEncoder : ProtobufTaggedBase(), Encoder, C serializer: SerializationStrategy, value: T? ) { - val elementKind = descriptor.getElementDescriptor(index).kind - nullableMode = if (descriptor.isElementOptional(index)) { + nullableMode = if (descriptor.isElementOptional(index)) NullableMode.OPTIONAL - } else if (elementKind == StructureKind.MAP || elementKind == StructureKind.LIST) { + else if (descriptor.getElementDescriptor(index).kind.isMapOrList()) NullableMode.COLLECTION - } else { + else NullableMode.ACCEPTABLE - } pushTag(descriptor.getTag(index)) encodeNullableSerializableValue(serializer, value) diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufNothingTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufNothingTest.kt index e90ff2be0..bdc93c231 100644 --- a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufNothingTest.kt +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufNothingTest.kt @@ -5,7 +5,6 @@ package kotlinx.serialization.protobuf import kotlinx.serialization.* -import kotlinx.serialization.test.* import kotlin.test.* class ProtobufNothingTest { @@ -13,17 +12,16 @@ class ProtobufNothingTest { /*private*/ data class NullableNothingBox(val value: Nothing?) // `private` doesn't work on the JS legacy target @Serializable - private data class ParameterizedBox(val value: T?) + private data class NullablePropertyNotNullUpperBoundParameterizedBox(val value: T?) + + @Serializable + private data class NullableUpperBoundParameterizedBox(val value: T) - private inline fun testConversion(data: T, expectedHexString: String) { - val string = ProtoBuf.encodeToHexString(data).uppercase() - assertEquals(expectedHexString, string) - assertEquals(data, ProtoBuf.decodeFromHexString(string)) - } @Test fun testNothing() { testConversion(NullableNothingBox(null), "") - testConversion(ParameterizedBox(null), "") + testConversion(NullablePropertyNotNullUpperBoundParameterizedBox(null), "") + testConversion(NullableUpperBoundParameterizedBox(null), "") } } diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufPrimitivesTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufPrimitivesTest.kt index 56c7bfab1..f8716446d 100644 --- a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufPrimitivesTest.kt +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufPrimitivesTest.kt @@ -3,18 +3,10 @@ */ package kotlinx.serialization.protobuf -import kotlinx.serialization.* import kotlinx.serialization.builtins.* import kotlin.test.* class ProtobufPrimitivesTest { - - private fun testConversion(data: T, serializer: KSerializer, expectedHexString: String) { - val string = ProtoBuf.encodeToHexString(serializer, data).uppercase() - assertEquals(expectedHexString, string) - assertEquals(data, ProtoBuf.decodeFromHexString(serializer, string)) - } - @Test fun testPrimitiveTypes() { testConversion(true, Boolean.serializer(), "01") diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufTypeParameterTest.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufTypeParameterTest.kt new file mode 100644 index 000000000..0b0f19f4c --- /dev/null +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufTypeParameterTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.protobuf + +import kotlinx.serialization.* +import kotlin.test.* + +class ProtobufTypeParameterTest { + @Serializable + data class Box(val value: T) + + @Serializable + data class ExplicitNullableUpperBoundBox(val value: T) + + @Serializable + data class ExplicitNullableUpperNullablePropertyBoundBox(val value: T?) + + inline fun testBox(value: T, expectedHexString: String) { + testConversion(Box(value), expectedHexString) + testConversion(ExplicitNullableUpperBoundBox(value), expectedHexString) + testConversion(ExplicitNullableUpperNullablePropertyBoundBox(value), expectedHexString) + } + + @Serializable + private data class DefaultArgPair(val first: T, val second: T = first) + + companion object { + val testList0 = emptyList() + val testList1 = listOf(0) + val testMap0 = emptyMap() + val testMap1 = mapOf(0 to 0) + } + + + @Test + fun testNothingBoxesWithNull() { + // Cannot use 'Nothing?' as reified type parameter + //testBox(null, "") + testConversion(Box(null), "") + testConversion(ExplicitNullableUpperBoundBox(null), "") + @Suppress("RemoveExplicitTypeArguments") + testConversion(ExplicitNullableUpperNullablePropertyBoundBox(null), "") + testConversion(ExplicitNullableUpperNullablePropertyBoundBox(null), "") + } + + @Test + fun testIntBoxes() { + testBox(0, "0800") + testBox(1, "0801") + } + + @Test + fun testNullableIntBoxes() { + testBox(null, "") + testBox(0, "0800") + } + + @Test + fun testCollectionBoxes() { + testBox(testList0, "") + testBox(testList1, "0800") + testBox(testMap0, "") + testBox(testMap1, "0A0408001000") + } + + @Test + fun testNullableCollectionBoxes() { + fun assertFailsForNullForCollectionTypes(block: () -> Unit) { + try { + block() + fail() + } catch (e: SerializationException) { + assertEquals( + "'null' is not supported for collection types in ProtoBuf", e.message + ) + } + } + assertFailsForNullForCollectionTypes { + testBox?>(null, "") + } + assertFailsForNullForCollectionTypes { + testBox?>(null, "") + } + testBox?>(testList0, "") + testBox?>(testMap0, "") + } + + @Test + fun testWithDefaultArguments() { + testConversion(DefaultArgPair(null), "") + testConversion(DefaultArgPair(1), "0801") + testConversion(DefaultArgPair(null, null), "") + testConversion(DefaultArgPair(null, 1), "1001") + assertFailsWith { + testConversion(DefaultArgPair(1, null), "0801") + } + testConversion(DefaultArgPair(1, 1), "0801") + testConversion(DefaultArgPair(1, 2), "08011002") + } +} \ No newline at end of file diff --git a/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/TestFunctions.kt b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/TestFunctions.kt new file mode 100644 index 000000000..7302c9953 --- /dev/null +++ b/formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/TestFunctions.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.protobuf + +import kotlinx.serialization.* +import kotlin.test.* + +fun testConversion(data: T, serializer: KSerializer, expectedHexString: String) { + val string = ProtoBuf.encodeToHexString(serializer, data).uppercase() + assertEquals(expectedHexString, string) + assertEquals(data, ProtoBuf.decodeFromHexString(serializer, string)) +} + +inline fun testConversion(data: T, expectedHexString: String) { + val string = ProtoBuf.encodeToHexString(data).uppercase() + assertEquals(expectedHexString, string) + assertEquals(data, ProtoBuf.decodeFromHexString(string)) +}