From 094fe23f9005b7e23c1ad8d6f59eaf1918b1de2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fa=C3=A7=20Eldenk?= Date: Mon, 16 Oct 2023 23:53:06 +0300 Subject: [PATCH] fix missing points and add tests --- .../kotlinx/protobuf/gen/CodeGenerator.kt | 54 ++++++++++++++----- .../kotlinx/protobuf/gen/CodeGeneratorTest.kt | 23 ++++++++ .../messages/message_no_fields.proto.kt | 51 ++++++++++++++++++ .../kotlinx/protobuf/gen/proto/MessageTest.kt | 18 ++++++- testProtos/data_class.proto | 14 ----- testProtos/message_no_fields.proto | 22 ++++++++ 6 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 app/src/test/kotlin/dogacel/kotlinx/protobuf/gen/CodeGeneratorTest.kt create mode 100644 generated-code-tests/src/main/kotlin/testgen/messages/message_no_fields.proto.kt delete mode 100644 testProtos/data_class.proto create mode 100644 testProtos/message_no_fields.proto diff --git a/app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt b/app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt index 2445ec8..9acf99d 100644 --- a/app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt +++ b/app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt @@ -166,16 +166,42 @@ class CodeGenerator { } /** - * Generate the code for the given [Descriptors.Descriptor]. Returns a Boolean determine whether the - * descriptor has a property field or not. + * Check if the given message descriptor contains any fields or not. * - * @param messageDescriptor [Descriptors.FileDescriptor] to generate code for. - * @return Boolean that represents whether the descriptor has a property field or not. + * This is used to avoid generating data classes with 0 properties because they are not allowed in Kotlin. + * + * @param messageDescriptor the descriptor to check if it has any fields. + * @return whether the given message descriptor contains any fields or not. */ - private fun hasPropertyField(messageDescriptor: Descriptors.Descriptor): Boolean { - return messageDescriptor.fields.any { - it.isOptional || it.isRequired || it.isRepeated - } + fun hasNoField(messageDescriptor: Descriptors.Descriptor): Boolean { + return messageDescriptor.fields.isEmpty() + } + + /** + * Get the overriden methods for a method with no fields. + * + * @param messageDescriptor descriptor of the message to generate empty override methods for. + * @return a list of [FunSpec]s that contain the overriden `toString`, `hashCode` and `equals`. + */ + private fun getEmptyOverrideFunSpecs(messageDescriptor: Descriptors.Descriptor): List { + return listOf( + FunSpec.builder("toString") + .addModifiers(KModifier.OVERRIDE) + .addStatement("return %S", messageDescriptor.name) + .returns(String::class) + .build(), + FunSpec.builder("hashCode") + .addModifiers(KModifier.OVERRIDE) + .addStatement("return %L", messageDescriptor.name.hashCode()) + .returns(Int::class) + .build(), + FunSpec.builder("equals") + .addModifiers(KModifier.OVERRIDE) + .addParameter("other", Any::class.asTypeName().copy(nullable = true)) + .addStatement("return other is ${messageDescriptor.name}") + .returns(Boolean::class) + .build() + ) } /** @@ -208,13 +234,13 @@ class CodeGenerator { * @return [TypeSpec.Builder] that contains the generated code. */ private fun generateSingleClass(messageDescriptor: Descriptors.Descriptor): TypeSpec.Builder { - val typeSpec = if (hasPropertyField(messageDescriptor)) { - TypeSpec.classBuilder(messageDescriptor.name) - .addModifiers(KModifier.DATA) - .addAnnotation(Serializable::class) + val typeSpec = TypeSpec.classBuilder(messageDescriptor.name) + .addAnnotation(Serializable::class) + + if (hasNoField(messageDescriptor)) { + typeSpec.addFunctions(getEmptyOverrideFunSpecs(messageDescriptor)) } else { - TypeSpec.classBuilder(messageDescriptor.name) - .addAnnotation(Serializable::class) + typeSpec.addModifiers(KModifier.DATA) } // A Data class needs a primary constructor with all the parameters. diff --git a/app/src/test/kotlin/dogacel/kotlinx/protobuf/gen/CodeGeneratorTest.kt b/app/src/test/kotlin/dogacel/kotlinx/protobuf/gen/CodeGeneratorTest.kt new file mode 100644 index 0000000..ca30f5f --- /dev/null +++ b/app/src/test/kotlin/dogacel/kotlinx/protobuf/gen/CodeGeneratorTest.kt @@ -0,0 +1,23 @@ +package dogacel.kotlinx.protobuf.gen + +import messages.MessageNoFieldsOuterClass.MessageNoFields +import kotlin.test.Test +import kotlin.test.assertEquals + +class CodeGeneratorTest { + + @Test + fun hasAnyFieldShouldWork() { + val codeGenerator = CodeGenerator() + + val messageNoFields = MessageNoFields.getDescriptor() + val messageSomeFields1 = MessageNoFields.SubMessageNoFields.getDescriptor() + val messageSomeFields2 = MessageNoFields.SubMessageNoFields.SubMessageNoFieldsExtend.getDescriptor() + val messageSomeFieldsOneof = MessageNoFields.SubMessageOneofFields.getDescriptor() + + assertEquals(true, codeGenerator.hasNoField(messageNoFields)) + assertEquals(false, codeGenerator.hasNoField(messageSomeFields1)) + assertEquals(false, codeGenerator.hasNoField(messageSomeFields2)) + assertEquals(false, codeGenerator.hasNoField(messageSomeFieldsOneof)) + } +} diff --git a/generated-code-tests/src/main/kotlin/testgen/messages/message_no_fields.proto.kt b/generated-code-tests/src/main/kotlin/testgen/messages/message_no_fields.proto.kt new file mode 100644 index 0000000..87ac438 --- /dev/null +++ b/generated-code-tests/src/main/kotlin/testgen/messages/message_no_fields.proto.kt @@ -0,0 +1,51 @@ +package testgen.messages + +import kotlin.Any +import kotlin.Boolean +import kotlin.Int +import kotlin.String +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +public class MessageNoFields() { + override fun toString(): String = "MessageNoFields" + + override fun hashCode(): Int = 70_682_849 + + override fun equals(other: Any?): Boolean = other is MessageNoFields + + @Serializable + public data class SubMessageNoFields( + @ProtoNumber(number = 1) + public val subHello: SubMessageNoFields? = null, + ) { + @Serializable + public data class SubMessageNoFieldsExtend( + @ProtoNumber(number = 1) + public val type: Type = testgen.messages.MessageNoFields.Type.UNKNOWN, + ) + } + + @Serializable + public data class SubMessageOneofFields( + @ProtoNumber(number = 1) + public val someValue: Int? = null, + ) { + init { + require( + listOfNotNull( + someValue, + ).size <= 1 + ) { "Should only contain one of some_oneof." } + } + } + + @Serializable + public enum class Type { + @ProtoNumber(number = 0) + UNKNOWN, + @ProtoNumber(number = 1) + KNOWN, + } +} diff --git a/generated-code-tests/src/test/kotlin/dogacel/kotlinx/protobuf/gen/proto/MessageTest.kt b/generated-code-tests/src/test/kotlin/dogacel/kotlinx/protobuf/gen/proto/MessageTest.kt index d2f6eb1..c5d68bd 100644 --- a/generated-code-tests/src/test/kotlin/dogacel/kotlinx/protobuf/gen/proto/MessageTest.kt +++ b/generated-code-tests/src/test/kotlin/dogacel/kotlinx/protobuf/gen/proto/MessageTest.kt @@ -3,6 +3,7 @@ package dogacel.kotlinx.protobuf.gen.proto import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.encodeToByteArray import kotlinx.serialization.protobuf.ProtoBuf +import testgen.messages.MessageNoFields import testgen.messages.MessagesMessage import kotlin.test.Test import kotlin.test.assertEquals @@ -36,7 +37,10 @@ class MessageTest { assertEquals(message.id, result.id) assertEquals(message.optionalForeignMessage.c, result.optionalForeignMessage?.c) assertEquals(message.optionalNestedMessage.a, result.optionalNestedMessage?.a) - assertEquals(message.optionalNestedMessage.corecursive.id, result.optionalNestedMessage?.corecursive?.id) + assertEquals( + message.optionalNestedMessage.corecursive.id, + result.optionalNestedMessage?.corecursive?.id + ) assertEquals( message.optionalNestedMessage.corecursive.optionalForeignMessage.c, result.optionalNestedMessage?.corecursive?.optionalForeignMessage?.c @@ -60,4 +64,16 @@ class MessageTest { val deser = messages.Messages.MessagesMessage.parseFrom(ProtoBuf.encodeToByteArray(result)) assertEquals(message, deser) } + + @Test + fun shouldSerNoField() { + val message = messages.MessageNoFieldsOuterClass.MessageNoFields.newBuilder().build() + + val messageBytes = message.toByteArray() + val result: MessageNoFields = ProtoBuf.decodeFromByteArray(messageBytes) + + val deser = + messages.MessageNoFieldsOuterClass.MessageNoFields.parseFrom(ProtoBuf.encodeToByteArray(result)) + assertEquals(message, deser) + } } diff --git a/testProtos/data_class.proto b/testProtos/data_class.proto deleted file mode 100644 index 5b9c9e8..0000000 --- a/testProtos/data_class.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto3"; - -message Hello { - enum Type { - UNKNOWN = 0; - KNOWN = 1; - } - message SubHello { - message HelloExtend { - optional Type type = 1; - } - optional SubHello subHello = 1; - } -} diff --git a/testProtos/message_no_fields.proto b/testProtos/message_no_fields.proto new file mode 100644 index 0000000..77d8d51 --- /dev/null +++ b/testProtos/message_no_fields.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package messages; + +message MessageNoFields { + enum Type { + UNKNOWN = 0; + KNOWN = 1; + } + message SubMessageNoFields { + message SubMessageNoFieldsExtend { + Type type = 1; + } + optional SubMessageNoFields subHello = 1; + } + + message SubMessageOneofFields { + oneof some_oneof { + int32 some_value = 1; + } + } +}