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 c8ffbf7..9acf99d 100644 --- a/app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt +++ b/app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable import java.nio.file.Path import kotlin.io.path.Path + /** * Links are used to keep track of the [TypeName] of the fields and the types they reference before the code * is generated. This way, code generation of types do not depend on each other and can be generated in any @@ -164,6 +165,45 @@ class CodeGenerator { return fileSpec } + /** + * Check if the given message descriptor contains any fields 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. + */ + 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() + ) + } + /** * Generate a single parameter for the given [Descriptors.FieldDescriptor]. Returns a * [ParameterSpec.Builder] so users can add additional code to the parameter. @@ -195,9 +235,14 @@ class CodeGenerator { */ private fun generateSingleClass(messageDescriptor: Descriptors.Descriptor): TypeSpec.Builder { val typeSpec = TypeSpec.classBuilder(messageDescriptor.name) - .addModifiers(KModifier.DATA) .addAnnotation(Serializable::class) + if (hasNoField(messageDescriptor)) { + typeSpec.addFunctions(getEmptyOverrideFunSpecs(messageDescriptor)) + } else { + typeSpec.addModifiers(KModifier.DATA) + } + // A Data class needs a primary constructor with all the parameters. val parameters = messageDescriptor.fields.map { generateSingleParameter(it).build() } val constructorSpec = FunSpec 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/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; + } + } +}