diff --git a/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Common.kt b/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Common.kt index f9d00ca..9804d0c 100644 --- a/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Common.kt +++ b/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Common.kt @@ -34,15 +34,16 @@ fun TypeContext.field( name: Identifier, number: FieldNumber, annotations: List, - descriptor: SerialDescriptor + descriptor: SerialDescriptor, + forceEncodeZeroValue: Boolean = false ): Field { val typeDescriptor = descriptor.actual val nullable = descriptor.isNullable || typeDescriptor.isNullable return when (val kind = typeDescriptor.kind) { is PrimitiveKind -> - field(name, scalar(annotations, kind), number, nullable) + field(name, scalar(annotations, kind), number, nullable, forceEncodeZeroValue) StructureKind.CLASS, StructureKind.OBJECT, SerialKind.ENUM, is PolymorphicKind -> - field(name, root.namedType(typeDescriptor), number, nullable) + field(name, root.namedType(typeDescriptor), number, nullable, forceEncodeZeroValue) SerialKind.CONTEXTUAL -> field( name, @@ -53,7 +54,7 @@ fun TypeContext.field( ) StructureKind.LIST -> if (typeDescriptor.getElementDescriptor(0).kind == PrimitiveKind.BYTE) - field(name, FieldEncoding.Bytes, number, nullable) + field(name, FieldEncoding.Bytes, number, nullable, forceEncodeZeroValue) else if (nullable) optionalListField(typeDescriptor, name, number, annotations) else @@ -70,12 +71,13 @@ private fun TypeContext.field( name: Identifier, type: FieldEncoding, number: FieldNumber, - optional: Boolean + optional: Boolean, + encodeZeroValue: Boolean ): Field = Field( name, type, number, if (optional) FieldRule.Optional else FieldRule.Singular, - fieldEncoder(type, number, optional), + fieldEncoder(type, number, encodeZeroValue || optional), fieldDecoder(type) ) diff --git a/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Polymorphic.kt b/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Polymorphic.kt index 2ae5ba8..72a858a 100644 --- a/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Polymorphic.kt +++ b/protobuf-kotlin-serialization/src/commonMain/kotlin/pro/felixo/protobuf/serialization/generation/internal/Polymorphic.kt @@ -60,30 +60,24 @@ private fun TypeContext.messageOfPolymorphicClass( } } -private fun TypeContext.fieldForSubType(subDescriptor: SerialDescriptor, numberIterator: FieldNumberIterator): Field { - val subTypeRef = root.namedType(subDescriptor) - val number = FieldNumber( - subDescriptor.annotations.filterIsInstance() - .firstOrNull()?.number - ?: numberIterator.next() - ) - return Field( - Identifier( - subTypeRef.name.value.replaceFirstChar { it.lowercaseChar() } +private fun TypeContext.fieldForSubType(subDescriptor: SerialDescriptor, numberIterator: FieldNumberIterator): Field = + field( + Identifier(simpleTypeName(subDescriptor).replaceFirstChar { it.lowercaseChar() }), + FieldNumber( + subDescriptor.annotations.filterIsInstance() + .firstOrNull()?.number + ?: numberIterator.next() ), - subTypeRef, - number, - encoder = fieldEncoder(subTypeRef, number, true), - decoder = fieldDecoder(subTypeRef) + emptyList(), + subDescriptor, + forceEncodeZeroValue = true ) -} -private fun fieldNumberIteratorFromSubTypes(descriptors: Iterable): FieldNumberIterator { - return FieldNumberIterator( +private fun fieldNumberIteratorFromSubTypes(descriptors: Iterable): FieldNumberIterator = + FieldNumberIterator( descriptors.mapNotNull { it.annotations.filterIsInstance().firstOrNull()?.number }.requireNoDuplicates { duplicatedNumber -> "Duplicate field number $duplicatedNumber in sub-types: ${descriptors.map { it.serialName }}" } ) -} diff --git a/protobuf-kotlin-serialization/src/commonTest/kotlin/pro/felixo/protobuf/serialization/integrationtests/InlineIntegrationTest.kt b/protobuf-kotlin-serialization/src/commonTest/kotlin/pro/felixo/protobuf/serialization/integrationtests/InlineIntegrationTest.kt index 9961959..e51c36b 100644 --- a/protobuf-kotlin-serialization/src/commonTest/kotlin/pro/felixo/protobuf/serialization/integrationtests/InlineIntegrationTest.kt +++ b/protobuf-kotlin-serialization/src/commonTest/kotlin/pro/felixo/protobuf/serialization/integrationtests/InlineIntegrationTest.kt @@ -320,6 +320,52 @@ class InlineIntegrationTest : StringSpec({ ) } + "creates class hierarchy with value sub-class" { + givenSchema( + SuperClass.serializer().descriptor, + encodeZeroValues = true + ) + verifySchema( + """ + message SuperClass { + oneof subtypes { + int32 subValueClassWithInt = 1; + string subValueClassWithString = 2; + } + } + """ + ) + verifyConversion( + SubValueClassWithInt(0), + "1: 0" + ) + verifyConversion( + SubValueClassWithInt(1), + "1: 1" + ) + verifyConversion( + SubValueClassWithString(""), + "2: {}" + ) + verifyConversion( + SubValueClassWithString("foo"), + """2: { "foo" }""" + ) + + givenSchema( + SuperClass.serializer().descriptor, + encodeZeroValues = false + ) + verifyConversion( + SubValueClassWithInt(0), + "1: 0" + ) + verifyConversion( + SubValueClassWithString(""), + "2: {}" + ) + } + "does not create synthetic top-level message from value class with custom serializer" { givenSchema(StringIntValueClass.serializer().descriptor) verifySchemaGenerationFails() @@ -454,4 +500,17 @@ class InlineIntegrationTest : StringSpec({ override fun serialize(encoder: Encoder, value: StringIntValueClass) = encoder.encodeString("${value.value}") } + + @Serializable + sealed interface SuperClass + + @JvmInline + @Serializable + @ProtoNumber(1) + value class SubValueClassWithInt(val int: Int) : SuperClass + + @JvmInline + @Serializable + @ProtoNumber(2) + value class SubValueClassWithString(val string: String) : SuperClass }