Skip to content

Commit

Permalink
add 'runtime-common' library to provide Well-known Type support
Browse files Browse the repository at this point in the history
  • Loading branch information
Dogacel committed Oct 7, 2023
1 parent 5dcaf4f commit 27d418e
Show file tree
Hide file tree
Showing 29 changed files with 911 additions and 219 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ deserialized to protobuf using [kotlinx.serialization](https://github.com/Kotlin
- [x] Generates Kotlin code for primitive fields such as `int32`, `string`, `bytes`.
- [x] Generates Kotlin code for `message`, `enum`, `repeated`, `map`, `oneof` types.
- [x] Generates Kotlin code that includes imports and uses nested types.
- [x] Supports common well-known types such as `Timestamp`, `StringValue` and serializes them to kotlin
primitives or standards.

## Roadmap

Expand All @@ -37,6 +39,7 @@ And here is a list of features that we are planning to work on after the first s
- [x] Support Well-Known Types deserialization to Well-Known Kotlin types such as `google.protobuf.Duration`
to `kotlin.time.Duration` and `google.protobuf.Timestamp` to `kotlinx.datetime.Instant`.
- An option is added to code generator to enable this feature.
- More WKT additions will be added.
- [ ] Support various options such as `deprecated`, `default`, `json_name`.
- [ ] Auto-generated comments from `.proto` files in the generated code.
- [ ] Support Protobuf JSON format by default.
Expand Down
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
id("org.jetbrains.kotlinx.kover")
application
`maven-publish`
id("org.jetbrains.dokka") version "1.8.20"
id("org.jetbrains.dokka")
}

repositories {
Expand All @@ -26,6 +26,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.6.0-RC")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")

implementation("com.squareup:kotlinpoet:1.14.2")

testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
Expand Down Expand Up @@ -139,7 +140,7 @@ publishing {
pom {
name.set("kotlinx-protobuf-gen")
description.set("Generate kotlin classes using kotlinx.serialization from proto definitions.")
url.set("https://github.com/dogace/kotlinx-protobuf-gen")
url.set("https://github.com/dogacel/kotlinx-protobuf-gen")
licenses {
license {
name.set("Apache-2.0")
Expand Down
20 changes: 16 additions & 4 deletions app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/CodeGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ class CodeGenerator {
}
}

/**
* Whether to generate a class for the given descriptor or not. Currently we do not create classes for
* well-known types.
*/
fun shouldGenerateClass(descriptor: Descriptors.Descriptor): Boolean {
return options.wellKnownTypes.getFor(descriptor) == null
}

/**
* Generate the code for the given [Descriptors.FileDescriptor]. Returns a [FileSpec.Builder] so users
* can add additional code to the file.
Expand All @@ -135,8 +143,10 @@ class CodeGenerator {
val fileSpec = FileSpec.builder(packageName, fileName)

fileDescriptor.messageTypes.forEach { messageType ->
val typeSpec = generateSingleClass(messageType)
fileSpec.addType(typeSpec.build())
if (shouldGenerateClass(messageType)) {
val typeSpec = generateSingleClass(messageType)
fileSpec.addType(typeSpec.build())
}
}

fileDescriptor.enumTypes.forEach { enumType ->
Expand Down Expand Up @@ -231,9 +241,11 @@ class CodeGenerator {
// Recursively generate nested classes and enums.
val nestedTypes = messageDescriptor.nestedTypes.filterNot {
it.options.mapEntry
}.map {
generateSingleClass(it).build()
}
.filter { shouldGenerateClass(it) }
.map {
generateSingleClass(it).build()
}
typeSpec.addTypes(nestedTypes)

val nestedEnums = messageDescriptor.enumTypes.map {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dogacel.kotlinx.protobuf.gen

import com.google.protobuf.Descriptors
import dogacel.kotlinx.protobuf.gen.wkt.DefaultWellKnownTypes
import dogacel.kotlinx.protobuf.gen.wkt.NoWellKnownTypes
import dogacel.kotlinx.protobuf.gen.wkt.WellKnownTypes

/**
* A set of options to customize the generated code.
Expand All @@ -21,7 +24,7 @@ data class CodeGeneratorOptions(
val generateServices: Boolean = true,
val generateGrpcServices: Boolean = true,
val generateGrpcMethodsSuspend: Boolean = true,
val wellKnownTypes: WellKnownTypes = DefaultWellKnownTypes,
val wellKnownTypes: WellKnownTypes = NoWellKnownTypes,
) {
companion object {
fun parse(parameter: String): CodeGeneratorOptions {
Expand All @@ -36,6 +39,9 @@ data class CodeGeneratorOptions(
useCamelCase = flags.contains("use_snake_case").not(),
generateServices = flags.contains("disable_services").not(),
generateGrpcMethodsSuspend = flags.contains("disable_grpc_methods_suspend").not(),
wellKnownTypes = flags.contains("use_well_known_types").let {
if (it) DefaultWellKnownTypes else NoWellKnownTypes
},
)
}
}
Expand Down
47 changes: 0 additions & 47 deletions app/src/main/kotlin/dogacel/kotlinx/protobuf/gen/WellKnownTypes.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package dogacel.kotlinx.protobuf.gen.wkt

import kotlinx.datetime.Instant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds

// @ProtoNumber(number = 12)
// public val optionalFieldMask: FieldMask? = null,
// @ProtoNumber(number = 13)
// public val optionalStruct: Struct? = null,
// @ProtoNumber(number = 14)
// public val optionalAny: Any? = null,
// @ProtoNumber(number = 15)
// public val optionalValue: Value? = null,
// @ProtoNumber(number = 16)
// public val repeatedListValue: ListValue? = null,

typealias BoolValueP = @Serializable(with = BoolValueSerializer::class) Boolean?
typealias Int32ValueP = @Serializable(with = Int32ValueSerializer::class) Int?
typealias Int64ValueP = @Serializable(with = Int64ValueSerializer::class) Long?
typealias UInt32ValueP = @Serializable(with = UInt32ValueSerializer::class) UInt?
typealias UInt64ValueP = @Serializable(with = UInt64ValueSerializer::class) ULong?
typealias FloatValueP = @Serializable(with = FloatValueSerializer::class) Float?
typealias DoubleValueP = @Serializable(with = DoubleValueSerializer::class) Double?
typealias StringValueP = @Serializable(with = StringValueSerializer::class) String?
typealias BytesValueP = @Serializable(with = BytesValueSerializer::class) ByteArray?
typealias DurationP = @Serializable(with = DurationSerializer::class) Duration?
typealias TimestampP = @Serializable(with = TimestampSerializer::class) Instant?

object BoolValueSerializer : KSerializer<Boolean?> {
@Serializable
private data class Boxed(
val `value`: Boolean = false
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: Boolean?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): Boolean? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object Int32ValueSerializer : KSerializer<Int?> {
@Serializable
private data class Boxed(
val `value`: Int = 0
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: Int?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): Int? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object Int64ValueSerializer : KSerializer<Long?> {
@Serializable
private data class Boxed(
val `value`: Long = 0L
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: Long?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): Long? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object UInt32ValueSerializer : KSerializer<UInt?> {
@Serializable
private data class Boxed(
val `value`: UInt = 0U
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: UInt?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): UInt? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object UInt64ValueSerializer : KSerializer<ULong?> {
@Serializable
private data class Boxed(
val `value`: ULong = 0UL
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: ULong?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): ULong? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object FloatValueSerializer : KSerializer<Float?> {
@Serializable
private data class Boxed(
val `value`: Float = 0f
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: Float?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): Float? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object DoubleValueSerializer : KSerializer<Double?> {
@Serializable
private data class Boxed(
val `value`: Double = 0.0
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: Double?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): Double? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}


object StringValueSerializer : KSerializer<String?> {
@Serializable
private data class Boxed(
val `value`: String = ""
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: String?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): String? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object BytesValueSerializer : KSerializer<ByteArray?> {
@Serializable
private data class Boxed(
val `value`: ByteArray = ByteArray(0)
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: ByteArray?) {
encoder.encodeNullableSerializableValue(Boxed.serializer(), value?.let { Boxed(it) })
}

override fun deserialize(decoder: Decoder): ByteArray? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.`value`
}
}

object DurationSerializer : KSerializer<Duration?> {
@Serializable
private data class Boxed(
val seconds: Long = 0L,
val nanos: Int = 0,
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: Duration?) {
encoder.encodeNullableSerializableValue(
Boxed.serializer(),
value?.toComponents { seconds, nanoseconds ->
Boxed(
seconds = seconds,
nanos = nanoseconds,
)
})
}

override fun deserialize(decoder: Decoder): Duration? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.let {
it.seconds.seconds + it.nanos.nanoseconds
}
}
}

object TimestampSerializer : KSerializer<Instant?> {
@Serializable
private data class Boxed(
val seconds: Long = 0L,
val nanos: Int = 0,
)

override val descriptor: SerialDescriptor = Boxed.serializer().descriptor
override fun serialize(encoder: Encoder, value: Instant?) {
encoder.encodeNullableSerializableValue(
Boxed.serializer(),
value?.let {
Boxed(
seconds = it.epochSeconds,
nanos = it.nanosecondsOfSecond,
)
},
)
}

override fun deserialize(decoder: Decoder): Instant? {
return decoder.decodeNullableSerializableValue(Boxed.serializer())?.let {
Instant.fromEpochSeconds(it.seconds, it.nanos)
}
}
}
Loading

0 comments on commit 27d418e

Please sign in to comment.