Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5d1a2bf
Support export/local and default_symbol_visibility.
TimOrtel Dec 6, 2025
b50d92d
Support (pb.java).nest_in_file_class
TimOrtel Dec 6, 2025
ea726e3
Add option target validation tests. Closes #111
TimOrtel Dec 7, 2025
0db15c1
Add (unsupported) handling for option and public import.
TimOrtel Dec 7, 2025
8b2530b
Only write top level enums for common source set.
TimOrtel Dec 7, 2025
5add000
add support for closed enums.
TimOrtel Dec 21, 2025
b9d7ad2
Set packed option unavailable on proto editions.
TimOrtel Dec 21, 2025
0a8dcda
Fix enum scoping.
TimOrtel Dec 21, 2025
980b041
Breaking: Proto enums are now generated as sealed interfaces. Removed…
TimOrtel Dec 23, 2025
dc394aa
Add proto2 parsing
TimOrtel Dec 26, 2025
8c04983
Add proto2 grammar
TimOrtel Dec 26, 2025
02ae5c1
Add support for group serialization
TimOrtel Dec 26, 2025
8565b90
Use kotlin IllegalArgumentException.
TimOrtel Dec 26, 2025
8e7854e
Merge branch 'feature/editions_2024' into feature/proto2
TimOrtel Dec 26, 2025
d1e8939
Merge branch 'main' into feature/proto2
TimOrtel Dec 26, 2025
fd24733
Add descriptor.proto to well known types.
TimOrtel Jan 6, 2026
e467a11
Add proto options import support.
TimOrtel Jan 6, 2026
f453641
Support default option on enums.
TimOrtel Feb 7, 2026
14d0e6f
Add symlink
TimOrtel Feb 8, 2026
1f16aa8
Add required field parsing support.
TimOrtel Feb 14, 2026
dc4546e
Revert source command.
TimOrtel Feb 14, 2026
b845d9e
Fix some tests.
TimOrtel Feb 14, 2026
d6391e8
Fix group message reading.
TimOrtel Feb 15, 2026
f5d24b3
Update readme.
TimOrtel Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

plugins {
`kotlin-dsl`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ actual interface Message {

actual val fullName: String

actual val isInitialized: Boolean

actual fun serialize(): ByteArray {
val buffer = Buffer()
serialize(CodedOutputStreamImpl(buffer))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.github.timortel.kmpgrpc.core

import io.github.timortel.kmpgrpc.core.message.Message

/**
* Thrown when a Protocol Buffers message is missing one or more required fields.
*
* In `proto2`, fields marked as `required` must be populated before a message
* can be fully initialized or serialized. This exception typically occurs during
* a DSL `build()` operation or when parsing a message that violates these
* presence constraints.
*
* @property msg The incomplete [Message] instance that triggered this exception.
* Note that accessing fields on this instance is safe, but it is considered
* semantically invalid according to the schema.
*/
class UninitializedMessageException(
val msg: Message,
) : RuntimeException(
"Message ${msg::class.simpleName} is missing required fields."
)
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ abstract class CodedInputStream {
return recursiveRead { deserializer.deserialize(this, extensionRegistry) }
}

fun <M : Message> readGroup(deserializer: MessageDeserializer<M>, extensionRegistry: ExtensionRegistry<M>, fieldNumber: Int): M {
checkRecursionLimit()
recursionDepth++
val message = deserializer.deserialize(this, extensionRegistry)
checkLastTagWas(wireFormatMakeTag(fieldNumber, WireFormat.END_GROUP))
recursionDepth--
return message
}

abstract fun readBytes(): ByteArray

abstract fun readUInt32(): UInt
Expand All @@ -89,6 +98,7 @@ abstract class CodedInputStream {
extensionRegistry: ExtensionRegistry<M>
): UnknownFieldOrExtension<M, Any>? {
val number = wireFormatGetTagFieldNumber(tag)
if (wireFormatGetTagWireType(tag) == WireFormat.END_GROUP.value) return null

val extension = extensionRegistry.getExtensionForFieldNumber(number)
@Suppress("UNCHECKED_CAST")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ interface CodedOutputStream {

fun writeMessage(fieldNumber: Int, value: Message)

fun writeGroup(fieldNumber: Int, value: Message)

fun writeMessageArray(fieldNumber: Int, values: List<Message>)

fun writeGroupArray(fieldNumber: Int, values: List<Message>)

fun writeSFixed32(fieldNumber: Int, value: Int)

fun writeSFixed32Array(fieldNumber: Int, values: List<Int>, tag: UInt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,16 @@ internal class CodedOutputStreamImpl(private val sink: Sink) : CodedOutputStream
values.forEach { writeMessage(fieldNumber, it) }
}

override fun writeGroup(fieldNumber: Int, value: Message) {
writeTag(fieldNumber, WireFormat.START_GROUP)
value.serialize(this)
writeTag(fieldNumber, WireFormat.END_GROUP)
}

override fun writeGroupArray(fieldNumber: Int, values: List<Message>) {
values.forEach { writeGroup(fieldNumber, it) }
}

override fun <K, V> writeMap(
fieldNumber: Int,
map: Map<K, V>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ expect interface Message {
*/
val fullName: String

/**
* If all required fields for this message have been set.
*/
val isInitialized: Boolean

/**
* Serializes this message and returns it as a [ByteArray].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fun <M : Message> mergeUnknownFieldOrExtension(
fieldOrExtension: UnknownFieldOrExtension<M, Any>?,
unknownFields: MutableList<UnknownField>,
extensionBuilder: MessageExtensionsBuilder<M>
) {
): Boolean {
when (fieldOrExtension) {
is UnknownFieldOrExtension.UnknownField -> unknownFields.add(fieldOrExtension.field)
is UnknownFieldOrExtension.RepeatedExtension<M, Any> -> {
Expand All @@ -18,6 +18,8 @@ fun <M : Message> mergeUnknownFieldOrExtension(
is UnknownFieldOrExtension.ScalarExtension<M, Any> -> {
extensionBuilder[fieldOrExtension.extension] = fieldOrExtension.value
}
null -> {}
null -> return false
}

return true
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ actual interface Message {

actual val requiredSize: Int

actual val isInitialized: Boolean

actual fun serialize(): ByteArray {
val buffer = Buffer()
serialize(CodedOutputStreamImpl(buffer))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ actual interface Message {

actual val requiredSize: Int

actual val isInitialized: Boolean

actual fun serialize(): ByteArray {
val buffer = Buffer()
serialize(CodedOutputStreamImpl(buffer))
Expand Down
9 changes: 7 additions & 2 deletions kmp-grpc-internal-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,16 @@ kmpGrpc {

includeWellKnownTypes = true

protoSourceFolders = project.files("src/commonMain/proto/general", "src/commonMain/proto/unknownfield", "src/commonMain/proto/editions")
protoSourceFolders = project.files(
"src/commonMain/proto/general",
"src/commonMain/proto/unknownfield",
"src/commonMain/proto/editions",
"src/commonMain/proto/proto2"
)
}

buildConfig {
packageName("iio.github.timortel.kmpgrpc.internal.test")
packageName("io.github.timortel.kmpgrpc.internal.test")

useKotlinOutput {
internalVisibility = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
syntax = "proto2";

package io.github.timortel.kmpgrpc.test.proto2;

option java_outer_classname = "Proto2GroupTest";

message A {
optional group B = 1 {
optional string field1 = 1;
optional int32 field2 = 2;

optional group C = 3 {
optional string field1 = 1;

optional string field2 = 2;
}
}

repeated group D = 2 {
optional string field1 = 1;
optional int32 field2 = 2;
}
}

message E {
optional string field1 = 1;
optional group G = 2 {
optional string field1 = 1;
extensions 2 to max;
}

extensions 3 to max;
}

extend E {
optional group F = 4 {
optional string field1 = 1;
}
}

extend E.G {
optional string field2 = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
syntax = "proto2";

package io.github.timortel.kmpgrpc.test.proto2;

option java_outer_classname = "Proto2RequiredFields";

message Proto2MessageWithMixedFields {
required string field1 = 1;
optional int32 field2 = 2;
}

message Proto2MessageWithRequiredFields {
required string field1 = 1;
required Proto2MessageWithMixedFields field2 = 2;
repeated Proto2MessageWithRequiredFields field3 = 3;
map<string, Proto2MessageWithRequiredFields> field4 = 4;

oneof x {
Proto2MessageWithMixedFields field5 = 5;
string field6 = 6;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto2";

package io.github.timortel.kmpgrpc.test.proto2;

import "proto2-group-test.proto";

option java_multiple_files = true;

service Proto2TestService {
rpc sendMessageWithNestedGroups (A) returns (A);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.timortel.kotlin_multiplatform_grpc_plugin.test
import ExtensionsTest
import io.github.timortel.kmpgrpc.core.message.extensions.buildExtensions
import io.github.timortel.kmpgrpc.test.*
import io.github.timortel.kmpgrpc.test.proto2.Proto2GroupTest

fun createScalarMessage() = scalarTypes {
field1 = "Test"
Expand Down Expand Up @@ -102,11 +103,13 @@ fun createMessageWithAllExtensions() = ExtensionsTest.MessageWithEveryExtension(
set(ExtensionsTest.field33, listOf(0uL, 134uL, 353111345134uL))
set(ExtensionsTest.field34, listOf(-14, 0, 1241522))
set(ExtensionsTest.field35, listOf(-154L, 0L, 4514124121L))
set(ExtensionsTest.field36, listOf(
byteArrayOf(0, -127, 127),
byteArrayOf(-123, 1, 2),
byteArrayOf(3, 3, -6)
))
set(
ExtensionsTest.field36, listOf(
byteArrayOf(0, -127, 127),
byteArrayOf(-123, 1, 2),
byteArrayOf(3, 3, -6)
)
)
}
)

Expand Down Expand Up @@ -188,3 +191,32 @@ fun createEditionsNonPackedTypesMessage(): EditionsNonPackedTypesMessage = Editi
field12List = field12,
field13List = field13,
)

fun createProto2NestedGroupMessage() = Proto2GroupTest.A(
b = Proto2GroupTest.A.B(
field1 = "text1",
field2 = 2,
c = Proto2GroupTest.A.B.C(field1 = "text3", field2 = "text4")
),
dList = listOf(
Proto2GroupTest.A.D(field1 = "text5", field2 = 6),
Proto2GroupTest.A.D(field1 = "text7", field2 = 8)
)
)

fun createProto2GroupMessageWithExtensions() = Proto2GroupTest.E(
field1 = "text1",
g = Proto2GroupTest.E.G(
field1 = "text2",
extensions = buildExtensions {
set(Proto2GroupTest.field2, "text3")
}
),
extensions = buildExtensions {
set(
Proto2GroupTest.f, Proto2GroupTest.F(
field1 = "text4"
)
)
}
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.timortel.kotlin_multiplatform_grpc_plugin.test.integration

import io.github.timortel.kmpgrpc.core.Channel
import io.github.timortel.kmpgrpc.core.Code
import io.github.timortel.kmpgrpc.core.StatusException
import io.github.timortel.kmpgrpc.test.EditionsTestServiceStub
import io.github.timortel.kmpgrpc.test.editions.EditionsLegacyField
import io.github.timortel.kotlin_multiplatform_grpc_plugin.test.createEditionsNonPackedTypesMessage
Expand All @@ -9,6 +11,7 @@ import io.github.timortel.kotlin_multiplatform_grpc_plugin.test.createMessageWit
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

abstract class EditionsRpcTest : ServerTest {

Expand Down Expand Up @@ -39,9 +42,11 @@ abstract class EditionsRpcTest : ServerTest {
@Test
fun testLegacyRequiredFieldNoData() = runTest {
val message = EditionsLegacyField()
val response = stub.sendLegacyRequiredField(message)
val exception = assertFailsWith<StatusException> {
stub.sendLegacyRequiredField(message)
}

assertEquals(message, response)
assertEquals(Code.INTERNAL, exception.status.code)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import io.github.timortel.kmpgrpc.core.Code
import io.github.timortel.kmpgrpc.core.StatusException
import io.github.timortel.kmpgrpc.core.message.UnknownField
import io.github.timortel.kmpgrpc.test.*
import io.github.timortel.kmpgrpc.test.proto2.Proto2TestServiceStub
import io.github.timortel.kotlin_multiplatform_grpc_plugin.test.createMessageWithAllTypes
import io.github.timortel.kotlin_multiplatform_grpc_plugin.test.createProto2NestedGroupMessage
import io.github.timortel.kotlin_multiplatform_grpc_plugin.test.createScalarMessage
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -141,6 +143,15 @@ abstract class RpcTest : ServerTest {
assertEquals(baseMessage, returnedMessage)
}

@Test
fun testSendNestedGroupMessage() = runTest {
val message = createProto2NestedGroupMessage()

val stub = Proto2TestServiceStub(channel)
val returnedMessage = stub.sendMessageWithNestedGroups(message)
assertEquals(message, returnedMessage)
}

@Test
fun testFailedRpcThrowsKmStatusException() = runTest {
val message = simpleMessage { }
Expand Down
Loading