Skip to content

Commit

Permalink
Fix Gen Class Without Any Properties (#15)
Browse files Browse the repository at this point in the history
Fx generate class without  Any Properties

Fixes #20

---------

Co-authored-by: Doğaç Eldenk <[email protected]>
  • Loading branch information
tamnguyenhuy and Dogacel authored Oct 16, 2023
1 parent 01de3fd commit 668d2cd
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<FunSpec> {
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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
22 changes: 22 additions & 0 deletions testProtos/message_no_fields.proto
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

0 comments on commit 668d2cd

Please sign in to comment.