diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExtensions.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExtensions.kt index 7b66a5087..a69e37b7e 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExtensions.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/buf/BufExtensions.kt @@ -5,19 +5,19 @@ package kotlinx.rpc.buf import kotlinx.rpc.buf.tasks.BufExecTask +import kotlinx.rpc.proto.ProtocPlugin import org.gradle.api.Action import org.gradle.api.Project -import org.gradle.api.provider.Property -import org.gradle.kotlin.dsl.property -import java.io.File -import javax.inject.Inject -import kotlin.time.Duration -import kotlinx.rpc.proto.ProtocPlugin import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.property +import java.io.File +import javax.inject.Inject import kotlin.reflect.KClass +import kotlin.time.Duration /** * Options for the Buf tasks. @@ -100,14 +100,14 @@ public open class BufExtension @Inject constructor(objects: ObjectFactory) { * Allows registering custom Buf tasks that can operate on the generated workspace. */ public open class BufTasksExtension @Inject constructor(internal val project: Project) { - // TODO change to commonMain/commonTest in docs when it's supported KRPC-180 + /** * Registers a custom Buf task that operates on the generated workspace. * * Name conventions: * `lint` input for [name] will result in tasks * named 'bufLintMain' and 'bufLintTest' for Kotlin/JVM projects - * and 'bufLintJvmMain' and 'bufLintJvmTest' for Kotlin/Multiplatform projects. + * and 'bufLintCommonMain' and 'bufLintCommonTest' for Kotlin/Multiplatform projects. * * Note the by default 'test' task doesn't depend on 'main' task. */ @@ -127,14 +127,14 @@ public open class BufTasksExtension @Inject constructor(internal val project: Pr return provider } - // TODO change to commonMain/commonTest in docs when it's supported KRPC-180 + /** * Registers a custom Buf task that operates on the generated workspace. * * Name conventions: * `lint` input for [name] will result in tasks * named 'bufLintMain' and 'bufLintTest' for Kotlin/JVM projects - * and 'bufLintJvmMain' and 'bufLintJvmTest' for Kotlin/Multiplatform projects. + * and 'bufLintCommonMain' and 'bufLintCommonTest' for Kotlin/Multiplatform projects. * * Note the by default 'test' task doesn't depend on 'main' task. */ diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/DefaultGrpcExtension.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/DefaultGrpcExtension.kt index 83db5de9b..ed822914b 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/DefaultGrpcExtension.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/grpc/DefaultGrpcExtension.kt @@ -4,9 +4,6 @@ package kotlinx.rpc.grpc -import kotlinx.rpc.GRPC_KOTLIN_VERSION -import kotlinx.rpc.GRPC_VERSION -import kotlinx.rpc.PROTOBUF_VERSION import kotlinx.rpc.buf.BufExtension import kotlinx.rpc.buf.configureBufExecutable import kotlinx.rpc.buf.tasks.registerBufExecTask @@ -14,10 +11,7 @@ import kotlinx.rpc.buf.tasks.registerBufGenerateTask import kotlinx.rpc.buf.tasks.registerGenerateBufGenYamlTask import kotlinx.rpc.buf.tasks.registerGenerateBufYamlTask import kotlinx.rpc.proto.* -import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_JAVA -import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_KOTLIN import kotlinx.rpc.proto.ProtocPlugin.Companion.KOTLIN_MULTIPLATFORM -import kotlinx.rpc.proto.ProtocPlugin.Companion.PROTOBUF_JAVA import kotlinx.rpc.util.ensureDirectoryExists import org.gradle.api.Action import org.gradle.api.GradleException @@ -61,9 +55,6 @@ internal open class DefaultGrpcExtension @Inject constructor( createDefaultProtocPlugins() project.protoSourceSets.forEach { protoSourceSet -> - protoSourceSet.protocPlugin(protocPlugins.protobufJava) - protoSourceSet.protocPlugin(protocPlugins.grpcJava) - protoSourceSet.protocPlugin(protocPlugins.grpcKotlin) protoSourceSet.protocPlugin(protocPlugins.kotlinMultiplatform) } @@ -294,30 +285,6 @@ internal open class DefaultGrpcExtension @Inject constructor( // ignore for bufGenerate task caching project.normalization.runtimeClasspath.ignore("**/protoc-gen-kotlin-multiplatform.log") project.normalization.runtimeClasspath.ignore("**/.keep") - - protocPlugins.create(GRPC_JAVA) { - isJava.set(true) - - remote { - locator.set("buf.build/grpc/java:v$GRPC_VERSION") - } - } - - protocPlugins.create(GRPC_KOTLIN) { - remote { - locator.set("buf.build/grpc/kotlin:v$GRPC_KOTLIN_VERSION") - } - } - - protocPlugins.create(PROTOBUF_JAVA) { - isJava.set(true) - - remote { - // for some reason they omit the first digit in this version: - // https://buf.build/protocolbuffers/java?version=v31.1 - locator.set("buf.build/protocolbuffers/java:v${PROTOBUF_VERSION.substringAfter(".")}") - } - } } private fun DefaultProtoSourceSet.correspondingMainSourceSetOrNull(): DefaultProtoSourceSet? { diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/DefaultProtoSourceSet.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/DefaultProtoSourceSet.kt index 81822424a..6bc7d7858 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/DefaultProtoSourceSet.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/DefaultProtoSourceSet.kt @@ -96,13 +96,11 @@ internal fun Project.createProtoExtensions() { } project.withKotlinKmpExtension { - findOrCreateAndConfigure("jvmMain", null) - findOrCreateAndConfigure("jvmTest", null) findOrCreateAndConfigure("commonMain", null) findOrCreateAndConfigure("commonTest", null) sourceSets.configureEach { - if (name == "jvmMain" || name == "jvmTest" || name == "commonMain" || name == "commonTest") { + if (name == "commonMain" || name == "commonTest") { findOrCreateAndConfigure(name, this) } } diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtocPlugin.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtocPlugin.kt index 8edd61ff8..496169fe7 100644 --- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtocPlugin.kt +++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/ProtocPlugin.kt @@ -4,10 +4,7 @@ package kotlinx.rpc.proto -import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_JAVA -import kotlinx.rpc.proto.ProtocPlugin.Companion.GRPC_KOTLIN import kotlinx.rpc.proto.ProtocPlugin.Companion.KOTLIN_MULTIPLATFORM -import kotlinx.rpc.proto.ProtocPlugin.Companion.PROTOBUF_JAVA import kotlinx.rpc.util.OS import org.gradle.api.Action import org.gradle.api.NamedDomainObjectContainer @@ -34,45 +31,6 @@ public fun NamedDomainObjectContainer.kotlinMultiplatform(action: kotlinMultiplatform.configure(action) } -/** - * Access to the `protobuf-java` protoc plugin. - */ -public val NamedDomainObjectContainer.protobufJava: NamedDomainObjectProvider - get() = named(PROTOBUF_JAVA) - -/** - * Configures the `protobuf-java` protoc plugin. - */ -public fun NamedDomainObjectContainer.protobufJava(action: Action) { - protobufJava.configure(action) -} - -/** - * Access to the `grpc-java` protoc plugin. - */ -public val NamedDomainObjectContainer.grpcJava: NamedDomainObjectProvider - get() = named(GRPC_JAVA) - -/** - * Configures the grpc-java protoc plugin. - */ -public fun NamedDomainObjectContainer.grpcJava(action: Action) { - grpcJava.configure(action) -} - -/** - * Access to the `grpc-kotlin` protoc plugin. - */ -public val NamedDomainObjectContainer.grpcKotlin: NamedDomainObjectProvider - get() = named(GRPC_KOTLIN) - -/** - * Configures the `grpc-kotlin` protoc plugin. - */ -public fun NamedDomainObjectContainer.grpcKotlin(action: Action) { - grpcKotlin.configure(action) -} - /** * Access to a specific protoc plugin. */ @@ -193,27 +151,6 @@ public open class ProtocPlugin( * @see [kotlinMultiplatform] */ public const val KOTLIN_MULTIPLATFORM: String = "kotlin-multiplatform" - - /** - * The name of the `protobuf-java` protoc plugin. - * - * @see [protobufJava] - */ - public const val PROTOBUF_JAVA: String = "java" - - /** - * The name of the `grpc-java` protoc plugin. - * - * @see [grpcJava] - */ - public const val GRPC_JAVA: String = "grpc-java" - - /** - * The name of the `grpc-kotlin` protoc plugin. - * - * @see [grpcKotlin] - */ - public const val GRPC_KOTLIN: String = "grpc-kotlin" } /** diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/derictory.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/directory.kt similarity index 100% rename from gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/derictory.kt rename to gradle-plugin/src/main/kotlin/kotlinx/rpc/proto/directory.kt diff --git a/gradle-plugin/src/test/kotlin/kotlinx/rpc/GrpcJvmProjectTest.kt b/gradle-plugin/src/test/kotlin/kotlinx/rpc/GrpcJvmProjectTest.kt index 5de8dd0ce..a4977b2d2 100644 --- a/gradle-plugin/src/test/kotlin/kotlinx/rpc/GrpcJvmProjectTest.kt +++ b/gradle-plugin/src/test/kotlin/kotlinx/rpc/GrpcJvmProjectTest.kt @@ -135,12 +135,6 @@ class GrpcJvmProjectTest : GrpcBaseTest() { version: v2 clean: true plugins: - - remote: buf.build/protocolbuffers/java:v${TEST_PROTOBUF_VERSION.substringAfter(".")} - out: java - - remote: buf.build/grpc/java:v$TEST_GRPC_VERSION - out: grpc-java - - remote: buf.build/grpc/kotlin:v$TEST_GRPC_KOTLIN_VERSION - out: grpc-kotlin - local: [protoc-gen-kotlin-multiplatform] out: kotlin-multiplatform opt: @@ -174,7 +168,8 @@ inputs: } @Suppress("detekt.MaxLineLength") - private val cliFlagsRegex = "- Buf Arguments: \\[.*?, generate, --output, .*?, --include-imports, --include-wkt, --error-format, json, --config, some\\.buf\\.yaml, --log-format, json, --timeout, 60s]".toRegex() + private val cliFlagsRegex = + "- Buf Arguments: \\[.*?, generate, --output, .*?, --include-imports, --include-wkt, --error-format, json, --config, some\\.buf\\.yaml, --log-format, json, --timeout, 60s]".toRegex() @Test fun `Custom Buf CLI Flags`() = runGrpcTest { diff --git a/gradle-plugin/src/test/kotlin/kotlinx/rpc/base/GrpcBaseTest.kt b/gradle-plugin/src/test/kotlin/kotlinx/rpc/base/GrpcBaseTest.kt index e19263bc9..15e73d7a4 100644 --- a/gradle-plugin/src/test/kotlin/kotlinx/rpc/base/GrpcBaseTest.kt +++ b/gradle-plugin/src/test/kotlin/kotlinx/rpc/base/GrpcBaseTest.kt @@ -9,17 +9,7 @@ import org.gradle.testkit.runner.TaskOutcome import org.intellij.lang.annotations.Language import org.junit.jupiter.api.TestInstance import java.nio.file.Path -import kotlin.io.path.ExperimentalPathApi -import kotlin.io.path.absolutePathString -import kotlin.io.path.deleteRecursively -import kotlin.io.path.exists -import kotlin.io.path.extension -import kotlin.io.path.isRegularFile -import kotlin.io.path.pathString -import kotlin.io.path.readLines -import kotlin.io.path.relativeTo -import kotlin.io.path.walk -import kotlin.lazy +import kotlin.io.path.* import kotlin.test.assertEquals import kotlin.test.fail @@ -229,7 +219,7 @@ abstract class GrpcBaseTest : BaseTest() { } companion object { - private const val KMP_SOURCE_SET = "jvm" + private const val KMP_SOURCE_SET = "common" private val KMP_SOURCE_SET_CAPITAL = KMP_SOURCE_SET.replaceFirstChar(Char::uppercaseChar) private const val KOTLIN_MULTIPLATFORM_DIR = "kotlin-multiplatform" const val RPC_INTERNAL = "_rpc_internal" diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/jvmMain/proto/some.proto b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/commonMain/proto/some.proto similarity index 100% rename from gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/jvmMain/proto/some.proto rename to gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/commonMain/proto/some.proto diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/jvmTest/proto/other.proto b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/commonTest/proto/other.proto similarity index 100% rename from gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/jvmTest/proto/other.proto rename to gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/main_and_test_mixed_sources/src/commonTest/proto/other.proto diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/minimal_grpc_configuration/src/jvmMain/proto/some.proto b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/minimal_grpc_configuration/src/commonMain/proto/some.proto similarity index 100% rename from gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/minimal_grpc_configuration/src/jvmMain/proto/some.proto rename to gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/minimal_grpc_configuration/src/commonMain/proto/some.proto diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_grpc/build.gradle.kts b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_grpc/build.gradle.kts index 315141d9c..1b1d627a6 100644 --- a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_grpc/build.gradle.kts +++ b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_grpc/build.gradle.kts @@ -11,6 +11,6 @@ plugins { // should nonetheless be available protoSourceSets { - jvmMain {} - jvmTest {} + commonMain {} + commonTest {} } diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_jvm_targets/build.gradle.kts b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_jvm_targets/build.gradle.kts index b30e26cf5..0e712e9ca 100644 --- a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_jvm_targets/build.gradle.kts +++ b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/no_jvm_targets/build.gradle.kts @@ -16,6 +16,6 @@ kotlin { // should nonetheless be available protoSourceSets { - jvmMain {} - jvmTest {} + commonMain {} + commonTest {} } diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/test_only_sources/src/jvmTest/proto/some.proto b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/test_only_sources/src/commonTest/proto/some.proto similarity index 100% rename from gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/test_only_sources/src/jvmTest/proto/some.proto rename to gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/test_only_sources/src/commonTest/proto/some.proto diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/build.gradle.kts b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/build.gradle.kts index 85e4446bd..adf702591 100644 --- a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/build.gradle.kts +++ b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/build.gradle.kts @@ -16,8 +16,8 @@ kotlin { } protoSourceSets { - jvmMain {} - jvmTest {} + commonMain {} + commonTest {} } rpc { diff --git a/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/src/jvmMain/proto/some.proto b/gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/src/commonMain/proto/some.proto similarity index 100% rename from gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/src/jvmMain/proto/some.proto rename to gradle-plugin/src/test/resources/projects/GrpcKmpProjectTest/with_other_targets/src/commonMain/proto/some.proto diff --git a/grpc/grpc-core/build.gradle.kts b/grpc/grpc-core/build.gradle.kts index 2c74b1d5e..e2df67fd9 100644 --- a/grpc/grpc-core/build.gradle.kts +++ b/grpc/grpc-core/build.gradle.kts @@ -133,12 +133,6 @@ kotlin { } protoSourceSets { - jvmTest { - proto { - exclude("exclude/**") - } - } - commonTest { proto { exclude("exclude/**") @@ -158,15 +152,6 @@ rpc { } project.tasks.withType().configureEach { - - // TODO: Remove this once we remove JVM generation - // Set compile for common(native) option - if (name.endsWith("CommonTest")) { - protocPlugins.kotlinMultiplatform { - options.put("targetMode", "common") - } - } - if (name.endsWith("Test")) { dependsOn(gradle.includedBuild("protoc-gen").task(":jar")) } diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/GrpcServerTest.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/GrpcServerTest.kt new file mode 100644 index 000000000..d233b29d8 --- /dev/null +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/GrpcServerTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.core.test + +//import kotlinx.coroutines.sync.Mutex +//import kotlinx.coroutines.sync.withLock +//import kotlinx.coroutines.test.runTest +//import kotlinx.rpc.RpcServer +//import kotlinx.rpc.grpc.GrpcClient +//import kotlinx.rpc.grpc.GrpcServer +// +//abstract class GrpcServerTest { +// private val serverMutex = Mutex() +// +// abstract fun RpcServer.registerServices() +// +// protected fun runGrpcTest(test: suspend (GrpcClient) -> Unit, ) = runTest { +// serverMutex.withLock { +// val grpcClient = GrpcClient("localhost", 8080) { +// usePlaintext() +// } +// +// val grpcServer = GrpcServer(8080, builder = { +// registerServices() +// }) +// +// grpcServer.start() +// test(grpcClient) +// grpcServer.shutdown() +// grpcServer.awaitTermination() +// grpcClient.shutdown() +// grpcClient.awaitTermination() +// } +// } +//} diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/StreamingTest.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/StreamingTest.kt new file mode 100644 index 000000000..9649ea829 --- /dev/null +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/StreamingTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.core.test + +//import StreamingTestService +//import kotlinx.coroutines.flow.* +//import kotlinx.rpc.RpcServer +//import kotlinx.rpc.withService +//import kotlin.test.Test +//import kotlin.test.assertEquals +// +//class StreamingTestServiceImpl : StreamingTestService { +// override fun Server(message: kotlinx.rpc.grpc.test.References): Flow { +// return flow { emit(message); emit(message); emit(message) } +// } +// +// override suspend fun Client(message: Flow): kotlinx.rpc.grpc.test.References { +// return message.last() +// } +// +// override fun Bidi(message: Flow): Flow { +// return message +// } +//} +// +//class StreamingTest : GrpcServerTest() { +// override fun RpcServer.registerServices() { +// registerService { StreamingTestServiceImpl() } +// } +// +// @Test +// fun testServerStreaming() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// service.Server(kotlinx.rpc.grpc.test.References { +// other = kotlinx.rpc.grpc.test.Other { +// field = 42 +// } +// }).toList().run { +// assertEquals(3, size) +// +// forEach { +// assertEquals(42, it.other.field) +// } +// } +// } +// +// @Test +// fun testClientStreaming() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val result = service.Client(flow { +// repeat(3) { +// emit(kotlinx.rpc.grpc.test.References { +// other = kotlinx.rpc.grpc.test.Other { +// field = 42 + it +// } +// }) +// } +// }) +// +// assertEquals(44, result.other.field) +// } +// +// @Test +// fun testBidiStreaming() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// service.Bidi(flow { +// repeat(3) { +// emit(kotlinx.rpc.grpc.test.References { +// other = kotlinx.rpc.grpc.test.Other { +// field = 42 + it +// } +// }) +// } +// }).collectIndexed { i, it -> +// assertEquals(42 + i, it.other.field) +// } +// } +//} diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/TestPrimitiveService.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/TestPrimitiveService.kt new file mode 100644 index 000000000..ef4875a01 --- /dev/null +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/TestPrimitiveService.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.core.test + +//import kotlinx.rpc.RpcServer +//import kotlinx.rpc.grpc.test.AllPrimitives +//import kotlinx.rpc.grpc.test.PrimitiveService +//import kotlinx.rpc.withService +//import kotlin.test.Test +//import kotlin.test.assertEquals + +//class PrimitiveServiceImpl : PrimitiveService { +// override suspend fun Echo(message: AllPrimitives): AllPrimitives { +// return message +// } +//} +// +//class TestPrimitiveService : GrpcServerTest() { +// override fun RpcServer.registerServices() { +// registerService { PrimitiveServiceImpl() } +// } +// +// @Test +// fun testPrimitive(): Unit = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val result = service.Echo(AllPrimitives { +// int32 = 42 +// }) +// +// assertEquals(42, result.int32) +// } +//} diff --git a/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/TestReferenceService.kt b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/TestReferenceService.kt new file mode 100644 index 000000000..983d62507 --- /dev/null +++ b/grpc/grpc-core/src/commonTest/kotlin/kotlinx/rpc/grpc/core/test/TestReferenceService.kt @@ -0,0 +1,282 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.core.test + +//import Other +//import ReferenceTestService +//import References +//import kotlinx.rpc.RpcServer +//import kotlinx.rpc.grpc.test.AllPrimitives +//import kotlinx.rpc.grpc.test.Nested +//import kotlinx.rpc.grpc.test.OneOf +//import kotlinx.rpc.grpc.test.OptionalTypes +//import kotlinx.rpc.grpc.test.Repeated +//import kotlinx.rpc.grpc.test.TestMap +//import kotlinx.rpc.grpc.test.UsingEnum +//import kotlinx.rpc.withService +//import kotlin.test.Test +//import kotlin.test.assertContentEquals +//import kotlin.test.assertEquals +//import kotlin.test.assertNotNull + +//class ReferenceTestServiceImpl : ReferenceTestService { +// override suspend fun Get(message: References): kotlinx.rpc.grpc.test.References { +// return kotlinx.rpc.grpc.test.References { +// other = kotlinx.rpc.grpc.test.Other { +// field = message.other.arg.toInt() +// } +// +// primitive = message.other.arg +// } +// } +// +// override suspend fun Enum(message: UsingEnum): UsingEnum { +// return message +// } +// +// override suspend fun Optional(message: OptionalTypes): OptionalTypes { +// return message +// } +// +// override suspend fun Repeated(message: Repeated): Repeated { +// return message +// } +// +// override suspend fun Nested(message: Nested): Nested { +// return message +// } +// +// override suspend fun Map(message: TestMap): TestMap { +// return message +// } +// +// override suspend fun OneOf(message: OneOf): OneOf { +// return message +// } +//} +// +//class TestReferenceService : GrpcServerTest() { +// override fun RpcServer.registerServices() { +// registerService { ReferenceTestServiceImpl() } +// } +// +// @Test +// fun testReferenceService() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val arg = References { +// other = Other { +// arg = "42" +// } +// } +// +// val result = service.Get(arg) +// +// assertEquals("42", result.primitive) +// assertEquals(42, result.other.field) +// } +// +// @Test +// fun testEnum() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val result = service.Enum(UsingEnum { +// enum = kotlinx.rpc.grpc.test.Enum.ONE +// }) +// +// assertEquals(kotlinx.rpc.grpc.test.Enum.ONE, result.enum) +// } +// +// @Test +// fun testOptional() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val resultNotNull = service.Optional(OptionalTypes { +// name = "test" +// age = 42 +// reference = kotlinx.rpc.grpc.test.Other { +// field = 42 +// } +// }) +// +// assertEquals("test", resultNotNull.name) +// assertEquals(42, resultNotNull.age) +// assertEquals(42, resultNotNull.reference?.field) +// +// val resultNullable = service.Optional(OptionalTypes { +// name = null +// age = null +// reference = null +// }) +// +// assertEquals(null, resultNullable.name) +// assertEquals(null, resultNullable.age) +// assertEquals(null, resultNullable.reference) +// } +// +// @Test +// fun testRepeated() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val result = service.Repeated(Repeated { +// listFixed32 = listOf(0u, 1u, 2u) +// listInt32 = listOf(0, 1, 2) +// listString = listOf("test", "hello") +// listReference = listOf(kotlinx.rpc.grpc.test.References { +// other = kotlinx.rpc.grpc.test.Other { +// field = 42 +// } +// }) +// }) +// +// assertEquals(listOf("test", "hello"), result.listString) +// assertEquals(listOf(0u, 1u, 2u), result.listFixed32) +// assertEquals(listOf(0, 1, 2), result.listInt32) +// assertEquals(1, result.listReference.size) +// assertEquals(42, result.listReference[0].other.field) +// +// val resultEmpty = service.Repeated(Repeated {}) +// +// assertEquals(emptyList(), resultEmpty.listString) +// assertEquals(emptyList(), resultEmpty.listFixed32) +// assertEquals(emptyList(), resultEmpty.listInt32) +// assertEquals(emptyList(), resultEmpty.listReference) +// } +// +// @Test +// fun testNested() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val result = service.Nested(Nested { +// inner1 = Nested.Inner1 { +// inner11 = Nested.Inner1.Inner11 { +// reference21 = null +// reference12 = Nested.Inner1.Inner12 { +// recursion = null +// } +// enum = Nested.Inner2.NestedEnum.ZERO +// } +// +// inner22 = Nested.Inner1.Inner12 { +// recursion = Nested.Inner1.Inner12 { +// recursion = null +// } +// } +// +// string = "42_1" +// +// inner1 = null +// } +// +// inner2 = Nested.Inner2 { +// inner21 = Nested.Inner2.Inner21 { +// reference11 = Nested.Inner1.Inner11 { +// reference21 = null +// reference12 = Nested.Inner1.Inner12 { +// recursion = null +// } +// enum = Nested.Inner2.NestedEnum.ZERO +// } +// +// reference22 = Nested.Inner2.Inner22 { +// enum = Nested.Inner2.NestedEnum.ZERO +// } +// } +// +// inner22 = Nested.Inner2.Inner22 { +// enum = Nested.Inner2.NestedEnum.ZERO +// } +// string = "42_2" +// } +// +// string = "42" +// enum = Nested.Inner2.NestedEnum.ZERO +// }) +// +// // Assert Inner1.Inner11 +// assertEquals(null, result.inner1.inner11.reference21) +// assertEquals(null, result.inner1.inner11.reference12.recursion) +// assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner1.inner11.enum) +// +// // Assert Inner1.Inner12 +// assertNotNull(result.inner1.inner22.recursion) +// assertEquals(null, result.inner1.inner22.recursion?.recursion) +// +// // Assert Inner1 +// assertEquals("42_1", result.inner1.string) +// assertEquals(null, result.inner1.inner1) +// +// // Assert Inner2.Inner21 +// assertEquals(null, result.inner2.inner21.reference11.reference21) +// assertEquals(null, result.inner2.inner21.reference11.reference12.recursion) +// assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner2.inner21.reference11.enum) +// assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner2.inner21.reference22.enum) +// +// // Assert Inner2.Inner22 +// assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner2.inner22.enum) +// +// // Assert Inner2 +// assertEquals("42_2", result.inner2.string) +// +// // Assert root Nested +// assertEquals("42", result.string) +// assertEquals(Nested.Inner2.NestedEnum.ZERO, result.enum) +// } +// +// @Test +// fun testMap() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val result = service.Map(TestMap { +// primitives = mapOf("1" to 2, "2" to 1) +// references = mapOf("ref" to kotlinx.rpc.grpc.test.References { +// other = kotlinx.rpc.grpc.test.Other { +// field = 42 +// } +// }) +// }) +// +// assertEquals(mapOf("1" to 2L, "2" to 1L), result.primitives) +// assertEquals(mapOf("ref" to 42), result.references.mapValues { it.value.other.field }) +// } +// +// @Test +// fun testOneOf() = runGrpcTest { grpcClient -> +// val service = grpcClient.withService() +// val result1 = service.OneOf(OneOf { +// primitives = OneOf.Primitives.StringValue("42") +// references = OneOf.References.Other(kotlinx.rpc.grpc.test.Other { +// field = 42 +// }) +// mixed = OneOf.Mixed.Int64(42L) +// single = OneOf.Single.Bytes(byteArrayOf(42)) +// }) +// +// assertEquals("42", (result1.primitives as OneOf.Primitives.StringValue).value) +// assertEquals(42, (result1.references as OneOf.References.Other).value.field) +// assertEquals(42L, (result1.mixed as OneOf.Mixed.Int64).value) +// assertContentEquals(byteArrayOf(42), (result1.single as OneOf.Single.Bytes).value) +// +// val result2 = service.OneOf(OneOf { +// primitives = OneOf.Primitives.Bool(true) +// references = OneOf.References.InnerReferences(kotlinx.rpc.grpc.test.References { +// other = kotlinx.rpc.grpc.test.Other { +// field = 42 +// } +// }) +// mixed = OneOf.Mixed.AllPrimitives(AllPrimitives { +// string = "42" +// }) +// }) +// +// assertEquals(true, (result2.primitives as OneOf.Primitives.Bool).value) +// assertEquals(42, (result2.references as OneOf.References.InnerReferences).value.other.field) +// assertEquals("42", (result2.mixed as OneOf.Mixed.AllPrimitives).value.string) +// assertEquals(null, result2.single) +// +// val result3 = service.OneOf(OneOf { +// primitives = OneOf.Primitives.Int32(42) +// }) +// +// assertEquals(42, (result3.primitives as OneOf.Primitives.Int32).value) +// assertEquals(null, result3.references) +// assertEquals(null, result3.mixed) +// assertEquals(null, result3.single) +// } +//} diff --git a/grpc/grpc-core/src/commonTest/proto/all_primitives.proto b/grpc/grpc-core/src/commonTest/proto/all_primitives.proto index a5e3650ef..14772b74a 100644 --- a/grpc/grpc-core/src/commonTest/proto/all_primitives.proto +++ b/grpc/grpc-core/src/commonTest/proto/all_primitives.proto @@ -9,13 +9,13 @@ message AllPrimitivesCommon { int64 int64 = 4; uint32 uint32 = 5; uint64 uint64 = 6; - sint32 sint32 = 7; - sint64 sint64 = 8; - fixed32 fixed32 = 9; - fixed64 fixed64 = 10; - sfixed32 sfixed32 = 11; - sfixed64 sfixed64 = 12; - bool bool = 13; - string string = 14; - bytes bytes = 15; + optional sint32 sint32 = 7; + optional sint64 sint64 = 8; + optional fixed32 fixed32 = 9; + optional fixed64 fixed64 = 10; + optional sfixed32 sfixed32 = 11; + optional sfixed64 sfixed64 = 12; + optional bool bool = 13; + optional string string = 14; + optional bytes bytes = 15; } diff --git a/grpc/grpc-core/src/jvmTest/proto/exclude/empty_deprecated.proto b/grpc/grpc-core/src/commonTest/proto/exclude/empty_deprecated.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/exclude/empty_deprecated.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/empty_deprecated.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/enum.proto b/grpc/grpc-core/src/commonTest/proto/exclude/enum.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/enum.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/enum.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/exclude/enum_options.proto b/grpc/grpc-core/src/commonTest/proto/exclude/enum_options.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/exclude/enum_options.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/enum_options.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/exclude/example.proto b/grpc/grpc-core/src/commonTest/proto/exclude/example.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/exclude/example.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/example.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/image-recognizer.proto b/grpc/grpc-core/src/commonTest/proto/exclude/image-recognizer.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/image-recognizer.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/image-recognizer.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/exclude/multiple_files.proto b/grpc/grpc-core/src/commonTest/proto/exclude/multiple_files.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/exclude/multiple_files.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/multiple_files.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/nested.proto b/grpc/grpc-core/src/commonTest/proto/exclude/nested.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/nested.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/nested.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/one_of.proto b/grpc/grpc-core/src/commonTest/proto/exclude/one_of.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/one_of.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/one_of.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/optional.proto b/grpc/grpc-core/src/commonTest/proto/exclude/optional.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/optional.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/optional.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/exclude/options.proto b/grpc/grpc-core/src/commonTest/proto/exclude/options.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/exclude/options.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/options.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/primitive_service.proto b/grpc/grpc-core/src/commonTest/proto/exclude/primitive_service.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/primitive_service.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/primitive_service.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/reference.proto b/grpc/grpc-core/src/commonTest/proto/exclude/reference.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/reference.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/reference.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/reference_package.proto b/grpc/grpc-core/src/commonTest/proto/exclude/reference_package.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/reference_package.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/reference_package.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/reference_service.proto b/grpc/grpc-core/src/commonTest/proto/exclude/reference_service.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/reference_service.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/reference_service.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/streaming.proto b/grpc/grpc-core/src/commonTest/proto/exclude/streaming.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/streaming.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/streaming.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/test_map.proto b/grpc/grpc-core/src/commonTest/proto/exclude/test_map.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/test_map.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/test_map.proto diff --git a/grpc/grpc-core/src/jvmTest/proto/exclude/with_comments.proto b/grpc/grpc-core/src/commonTest/proto/exclude/with_comments.proto similarity index 100% rename from grpc/grpc-core/src/jvmTest/proto/exclude/with_comments.proto rename to grpc/grpc-core/src/commonTest/proto/exclude/with_comments.proto diff --git a/grpc/grpc-core/src/commonTest/proto/repeated.proto b/grpc/grpc-core/src/commonTest/proto/repeated.proto index 002e421eb..55d1a2a19 100644 --- a/grpc/grpc-core/src/commonTest/proto/repeated.proto +++ b/grpc/grpc-core/src/commonTest/proto/repeated.proto @@ -6,4 +6,8 @@ message RepeatedCommon { repeated fixed32 listFixed32 = 1 [packed = true]; repeated int32 listInt32 = 2 [packed = false]; repeated string listString = 3; + + message InnerClass { + + } } diff --git a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/GrpcServerTest.kt b/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/GrpcServerTest.kt deleted file mode 100644 index 7204d639e..000000000 --- a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/GrpcServerTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.grpc.core.test - -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.test.runTest -import kotlinx.rpc.RpcServer -import kotlinx.rpc.grpc.GrpcClient -import kotlinx.rpc.grpc.GrpcServer - -abstract class GrpcServerTest { - private val serverMutex = Mutex() - - abstract fun RpcServer.registerServices() - - protected fun runGrpcTest(test: suspend (GrpcClient) -> Unit, ) = runTest { - serverMutex.withLock { - val grpcClient = GrpcClient("localhost", 8080) { - usePlaintext() - } - - val grpcServer = GrpcServer(8080, builder = { - registerServices() - }) - - grpcServer.start() - test(grpcClient) - grpcServer.shutdown() - grpcServer.awaitTermination() - grpcClient.shutdown() - grpcClient.awaitTermination() - } - } -} diff --git a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/StreamingTest.kt b/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/StreamingTest.kt deleted file mode 100644 index f5a69160b..000000000 --- a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/StreamingTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.grpc.core.test - -import StreamingTestService -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectIndexed -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.last -import kotlinx.coroutines.flow.toList -import kotlinx.rpc.RpcServer -import kotlinx.rpc.grpc.test.invoke -import kotlinx.rpc.registerService -import kotlinx.rpc.withService -import kotlin.test.Test -import kotlin.test.assertEquals - -class StreamingTestServiceImpl : StreamingTestService { - override fun Server(message: kotlinx.rpc.grpc.test.References): Flow { - return flow { emit(message); emit(message); emit(message) } - } - - override suspend fun Client(message: Flow): kotlinx.rpc.grpc.test.References { - return message.last() - } - - override fun Bidi(message: Flow): Flow { - return message - } -} - -class StreamingTest : GrpcServerTest() { - override fun RpcServer.registerServices() { - registerService { StreamingTestServiceImpl() } - } - - @Test - fun testServerStreaming() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - service.Server(kotlinx.rpc.grpc.test.References { - other = kotlinx.rpc.grpc.test.Other { - field= 42 - } - }).toList().run { - assertEquals(3, size) - - forEach { - assertEquals(42, it.other.field) - } - } - } - - @Test - fun testClientStreaming() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val result = service.Client(flow { - repeat(3) { - emit(kotlinx.rpc.grpc.test.References { - other = kotlinx.rpc.grpc.test.Other { - field = 42 + it - } - }) - } - }) - - assertEquals(44, result.other.field) - } - - @Test - fun testBidiStreaming() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - service.Bidi(flow { - repeat(3) { - emit(kotlinx.rpc.grpc.test.References { - other = kotlinx.rpc.grpc.test.Other { - field = 42 + it - } - }) - } - }).collectIndexed { i, it -> - assertEquals(42 + i, it.other.field) - } - } -} diff --git a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/TestPrimitiveService.kt b/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/TestPrimitiveService.kt deleted file mode 100644 index b15b3ff20..000000000 --- a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/TestPrimitiveService.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.grpc.core.test - -import kotlinx.rpc.RpcServer -import kotlinx.rpc.grpc.test.AllPrimitives -import kotlinx.rpc.grpc.test.invoke -import kotlinx.rpc.grpc.test.PrimitiveService -import kotlinx.rpc.registerService -import kotlinx.rpc.withService -import kotlin.test.Test -import kotlin.test.assertEquals - -class PrimitiveServiceImpl : PrimitiveService { - override suspend fun Echo(message: AllPrimitives): AllPrimitives { - return message - } -} - -class TestPrimitiveService : GrpcServerTest() { - override fun RpcServer.registerServices() { - registerService { PrimitiveServiceImpl() } - } - - @Test - fun testPrimitive(): Unit = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val result = service.Echo(AllPrimitives { - int32 = 42 - }) - - assertEquals(42, result.int32) - } -} diff --git a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/TestReferenceService.kt b/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/TestReferenceService.kt deleted file mode 100644 index 3eb7ff64a..000000000 --- a/grpc/grpc-core/src/jvmTest/kotlin/kotlinx/rpc/grpc/core/test/TestReferenceService.kt +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.grpc.core.test - -import ReferenceTestService -import References -import Other -import invoke -import kotlinx.rpc.RpcServer -import kotlinx.rpc.grpc.test.AllPrimitives -import kotlinx.rpc.grpc.test.Nested -import kotlinx.rpc.grpc.test.OneOf -import kotlinx.rpc.grpc.test.OptionalTypes -import kotlinx.rpc.grpc.test.Repeated -import kotlinx.rpc.grpc.test.TestMap -import kotlinx.rpc.grpc.test.UsingEnum -import kotlinx.rpc.grpc.test.invoke -import kotlinx.rpc.registerService -import kotlinx.rpc.withService -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -class ReferenceTestServiceImpl : ReferenceTestService { - override suspend fun Get(message: References): kotlinx.rpc.grpc.test.References { - return kotlinx.rpc.grpc.test.References { - other = kotlinx.rpc.grpc.test.Other { - field = message.other.arg.toInt() - } - - primitive = message.other.arg - } - } - - override suspend fun Enum(message: UsingEnum): UsingEnum { - return message - } - - override suspend fun Optional(message: OptionalTypes): OptionalTypes { - return message - } - - override suspend fun Repeated(message: Repeated): Repeated { - return message - } - - override suspend fun Nested(message: Nested): Nested { - return message - } - - override suspend fun Map(message: TestMap): TestMap { - return message - } - - override suspend fun OneOf(message: OneOf): OneOf { - return message - } -} - -class TestReferenceService : GrpcServerTest() { - override fun RpcServer.registerServices() { - registerService { ReferenceTestServiceImpl() } - } - - @Test - fun testReferenceService() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val arg = References { - other = Other { - arg = "42" - } - } - - val result = service.Get(arg) - - assertEquals("42", result.primitive) - assertEquals(42, result.other.field) - } - - @Test - fun testEnum() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val result = service.Enum(UsingEnum { - enum = kotlinx.rpc.grpc.test.Enum.ONE - }) - - assertEquals(kotlinx.rpc.grpc.test.Enum.ONE, result.enum) - } - - @Test - fun testOptional() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val resultNotNull = service.Optional(OptionalTypes { - name = "test" - age = 42 - reference = kotlinx.rpc.grpc.test.Other { - field = 42 - } - }) - - assertEquals("test", resultNotNull.name) - assertEquals(42, resultNotNull.age) - assertEquals(42, resultNotNull.reference?.field) - - val resultNullable = service.Optional(OptionalTypes { - name = null - age = null - reference = null - }) - - assertEquals(null, resultNullable.name) - assertEquals(null, resultNullable.age) - assertEquals(null, resultNullable.reference) - } - - @Test - fun testRepeated() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val result = service.Repeated(Repeated { - listFixed32 = listOf(0u, 1u, 2u) - listInt32 = listOf(0, 1, 2) - listString = listOf("test", "hello") - listReference = listOf(kotlinx.rpc.grpc.test.References { - other = kotlinx.rpc.grpc.test.Other { - field = 42 - } - }) - }) - - assertEquals(listOf("test", "hello"), result.listString) - assertEquals(listOf(0u, 1u, 2u), result.listFixed32) - assertEquals(listOf(0, 1, 2), result.listInt32) - assertEquals(1, result.listReference.size) - assertEquals(42, result.listReference[0].other.field) - - val resultEmpty = service.Repeated(Repeated {}) - - assertEquals(emptyList(), resultEmpty.listString) - assertEquals(emptyList(), resultEmpty.listFixed32) - assertEquals(emptyList(), resultEmpty.listInt32) - assertEquals(emptyList(), resultEmpty.listReference) - } - - @Test - fun testNested() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val result = service.Nested(Nested { - inner1 = Nested.Inner1 { - inner11 = Nested.Inner1.Inner11 { - reference21 = null - reference12 = Nested.Inner1.Inner12 { - recursion = null - } - enum = Nested.Inner2.NestedEnum.ZERO - } - - inner22 = Nested.Inner1.Inner12 { - recursion = Nested.Inner1.Inner12 { - recursion = null - } - } - - string = "42_1" - - inner1 = null - } - - inner2 = Nested.Inner2 { - inner21 = Nested.Inner2.Inner21 { - reference11 = Nested.Inner1.Inner11 { - reference21 = null - reference12 = Nested.Inner1.Inner12 { - recursion = null - } - enum = Nested.Inner2.NestedEnum.ZERO - } - - reference22 = Nested.Inner2.Inner22 { - enum = Nested.Inner2.NestedEnum.ZERO - } - } - - inner22 = Nested.Inner2.Inner22 { - enum = Nested.Inner2.NestedEnum.ZERO - } - string = "42_2" - } - - string = "42" - enum = Nested.Inner2.NestedEnum.ZERO - }) - - // Assert Inner1.Inner11 - assertEquals(null, result.inner1.inner11.reference21) - assertEquals(null, result.inner1.inner11.reference12.recursion) - assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner1.inner11.enum) - - // Assert Inner1.Inner12 - assertNotNull(result.inner1.inner22.recursion) - assertEquals(null, result.inner1.inner22.recursion?.recursion) - - // Assert Inner1 - assertEquals("42_1", result.inner1.string) - assertEquals(null, result.inner1.inner1) - - // Assert Inner2.Inner21 - assertEquals(null, result.inner2.inner21.reference11.reference21) - assertEquals(null, result.inner2.inner21.reference11.reference12.recursion) - assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner2.inner21.reference11.enum) - assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner2.inner21.reference22.enum) - - // Assert Inner2.Inner22 - assertEquals(Nested.Inner2.NestedEnum.ZERO, result.inner2.inner22.enum) - - // Assert Inner2 - assertEquals("42_2", result.inner2.string) - - // Assert root Nested - assertEquals("42", result.string) - assertEquals(Nested.Inner2.NestedEnum.ZERO, result.enum) - } - - @Test - fun testMap() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val result = service.Map(TestMap { - primitives = mapOf("1" to 2, "2" to 1) - references = mapOf("ref" to kotlinx.rpc.grpc.test.References { - other = kotlinx.rpc.grpc.test.Other { - field = 42 - } - }) - }) - - assertEquals(mapOf("1" to 2L, "2" to 1L), result.primitives) - assertEquals(mapOf("ref" to 42), result.references.mapValues { it.value.other.field }) - } - - @Test - fun testOneOf() = runGrpcTest { grpcClient -> - val service = grpcClient.withService() - val result1 = service.OneOf(OneOf { - primitives = OneOf.Primitives.StringValue("42") - references = OneOf.References.Other(kotlinx.rpc.grpc.test.Other { - field = 42 - }) - mixed = OneOf.Mixed.Int64(42L) - single = OneOf.Single.Bytes(byteArrayOf(42)) - }) - - assertEquals("42", (result1.primitives as OneOf.Primitives.StringValue).value) - assertEquals(42, (result1.references as OneOf.References.Other).value.field) - assertEquals(42L, (result1.mixed as OneOf.Mixed.Int64).value) - assertContentEquals(byteArrayOf(42), (result1.single as OneOf.Single.Bytes).value) - - val result2 = service.OneOf(OneOf { - primitives = OneOf.Primitives.Bool(true) - references = OneOf.References.InnerReferences(kotlinx.rpc.grpc.test.References { - other = kotlinx.rpc.grpc.test.Other { - field = 42 - } - }) - mixed = OneOf.Mixed.AllPrimitives(AllPrimitives { - string = "42" - }) - }) - - assertEquals(true, (result2.primitives as OneOf.Primitives.Bool).value) - assertEquals(42, (result2.references as OneOf.References.InnerReferences).value.other.field) - assertEquals("42", (result2.mixed as OneOf.Mixed.AllPrimitives).value.string) - assertEquals(null, result2.single) - - val result3 = service.OneOf(OneOf { - primitives = OneOf.Primitives.Int32(42) - }) - - assertEquals(42, (result3.primitives as OneOf.Primitives.Int32).value) - assertEquals(null, result3.references) - assertEquals(null, result3.mixed) - assertEquals(null, result3.single) - } -} diff --git a/grpc/grpc-core/src/jvmTest/proto/all_primitives.proto b/grpc/grpc-core/src/jvmTest/proto/all_primitives.proto deleted file mode 100644 index be698f9e6..000000000 --- a/grpc/grpc-core/src/jvmTest/proto/all_primitives.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "proto3"; - -package kotlinx.rpc.grpc.test; - -message AllPrimitives { - double double = 1; - float float = 2; - int32 int32 = 3; - int64 int64 = 4; - uint32 uint32 = 5; - uint64 uint64 = 6; - sint32 sint32 = 7; - sint64 sint64 = 8; - fixed32 fixed32 = 9; - fixed64 fixed64 = 10; - sfixed32 sfixed32 = 11; - sfixed64 sfixed64 = 12; - bool bool = 13; - string string = 14; - bytes bytes = 15; -} diff --git a/grpc/grpc-core/src/jvmTest/proto/repeated.proto b/grpc/grpc-core/src/jvmTest/proto/repeated.proto deleted file mode 100644 index c7256377a..000000000 --- a/grpc/grpc-core/src/jvmTest/proto/repeated.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto3"; - -package kotlinx.rpc.grpc.test; - -import 'reference_package.proto'; - -message Repeated { - repeated int32 listInt32 = 1; - repeated fixed32 listFixed32 = 2; - repeated string listString = 3; - repeated References listReference = 4; -} diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt index 006faee13..b1a6cdeff 100644 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt +++ b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt @@ -7,12 +7,8 @@ package kotlinx.rpc.protobuf import org.slf4j.Logger import org.slf4j.helpers.NOPLogger -data class CodeGenerationParameters( - val messageMode: RpcProtobufPlugin.MessageMode, -) open class CodeGenerator( - val parameters: CodeGenerationParameters, private val indent: String, private val builder: StringBuilder = StringBuilder(), private val logger: Logger = NOPLogger.NOP_LOGGER, @@ -68,7 +64,7 @@ open class CodeGenerator( } private fun withNextIndent(block: CodeGenerator.() -> Unit) { - CodeGenerator(parameters, "$indent$ONE_INDENT", builder, logger).block() + CodeGenerator("$indent$ONE_INDENT", builder, logger).block() } internal fun scope( @@ -132,7 +128,7 @@ open class CodeGenerator( return } - val nested = CodeGenerator(parameters, "$indent$ONE_INDENT", logger = logger).apply(block) + val nested = CodeGenerator("$indent$ONE_INDENT", logger = logger).apply(block) if (nested.isEmpty) { newLine() @@ -306,13 +302,12 @@ open class CodeGenerator( } class FileGenerator( - codeGenerationParameters: CodeGenerationParameters, var filename: String? = null, var packageName: String? = null, var packagePath: String? = packageName, var fileOptIns: List = emptyList(), logger: Logger = NOPLogger.NOP_LOGGER, -) : CodeGenerator(codeGenerationParameters, "", logger = logger) { +) : CodeGenerator("", logger = logger) { private val imports = mutableListOf() fun importPackage(name: String) { @@ -354,10 +349,9 @@ class FileGenerator( } fun file( - codeGenerationParameters: CodeGenerationParameters, name: String? = null, packageName: String? = null, logger: Logger = NOPLogger.NOP_LOGGER, block: FileGenerator.() -> Unit, -): FileGenerator = FileGenerator(codeGenerationParameters, name, packageName, packageName, emptyList(), logger) +): FileGenerator = FileGenerator(name, packageName, packageName, emptyList(), logger) .apply(block) diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinCommonGenerator.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinCommonGenerator.kt index 3a1e20198..fadeaebcb 100644 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinCommonGenerator.kt +++ b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinCommonGenerator.kt @@ -15,7 +15,6 @@ private const val RPC_INTERNAL_PACKAGE_SUFFIX = "_rpc_internal" class ModelToKotlinCommonGenerator( private val model: Model, private val logger: Logger, - private val codeGenerationParameters: CodeGenerationParameters, ) { fun generateKotlinFiles(): List { return model.files.flatMap { it.generateKotlinFiles() } @@ -36,7 +35,7 @@ class ModelToKotlinCommonGenerator( private fun FileDeclaration.generatePublicKotlinFile(): FileGenerator { currentPackage = packageName - return file(codeGenerationParameters, logger = logger) { + return file(logger = logger) { filename = this@generatePublicKotlinFile.name packageName = this@generatePublicKotlinFile.packageName.safeFullName() packagePath = this@generatePublicKotlinFile.packageName.safeFullName() @@ -61,7 +60,7 @@ class ModelToKotlinCommonGenerator( private fun FileDeclaration.generateInternalKotlinFile(): FileGenerator { currentPackage = packageName - return file(codeGenerationParameters, logger = logger) { + return file(logger = logger) { filename = this@generateInternalKotlinFile.name packageName = this@generateInternalKotlinFile.packageName.safeFullName() packagePath = @@ -213,7 +212,7 @@ class ModelToKotlinCommonGenerator( code("$assignment decoder.read$encFuncName()") } - is FieldType.List -> if (field.packed) { + is FieldType.List -> if (field.dec.isPacked) { whenCase("tag.fieldNr == ${field.number} && tag.wireType == WireType.LENGTH_DELIMITED") { code("$assignment decoder.readPacked${fieldType.value.decodeEncodeFuncName()}()") } @@ -239,30 +238,34 @@ class ModelToKotlinCommonGenerator( val fieldName = field.name if (field.nullable) { scope("$fieldName?.also") { - code(field.writeValue()) + code(field.writeValue("it")) } - } else if (!field.hasPresence) { + } else if (!field.dec.hasPresence()) { ifBranch(condition = field.defaultCheck(), ifBlock = { - code(field.writeValue()) + code(field.writeValue(field.name)) }) } else { - code(field.writeValue()) + code(field.writeValue(field.name)) } } } - private fun FieldDeclaration.writeValue(): String { + private fun FieldDeclaration.writeValue(variable: String): String { return when (val fieldType = type) { - is FieldType.IntegralType -> "encoder.write${type.decodeEncodeFuncName()}($number, $name)" + is FieldType.IntegralType -> "encoder.write${type.decodeEncodeFuncName()}($number, $variable)" is FieldType.List -> when { - packed && packedFixedSize -> - "encoder.writePacked${fieldType.value.decodeEncodeFuncName()}($number, $name)" + dec.isPacked && packedFixedSize -> + "encoder.writePacked${fieldType.value.decodeEncodeFuncName()}($number, $variable)" - packed && !packedFixedSize -> - "encoder.writePacked${fieldType.value.decodeEncodeFuncName()}($number, $name, ${wireSizeCall(name)})" + dec.isPacked && !packedFixedSize -> + "encoder.writePacked${fieldType.value.decodeEncodeFuncName()}($number, $variable, ${ + wireSizeCall( + variable + ) + })" else -> - "$name.forEach { encoder.write${fieldType.value.decodeEncodeFuncName()}($number, it) }" + "$variable.forEach { encoder.write${fieldType.value.decodeEncodeFuncName()}($number, it) }" } is FieldType.Map -> TODO() @@ -281,7 +284,7 @@ class ModelToKotlinCommonGenerator( } is FieldType.List -> when { - isPackable && !packedFixedSize -> sizeFunc + dec.isPacked && !packedFixedSize -> sizeFunc else -> error("Unexpected use of size call for field: $name, type: $fieldType") } @@ -427,13 +430,13 @@ class ModelToKotlinCommonGenerator( code("@kotlinx.rpc.grpc.annotations.Grpc") clazz(service.name.simpleName, declarationType = DeclarationType.Interface) { service.methods.forEach { method -> - val inputType by method.inputType - val outputType by method.outputType + val inputType = method.inputType + val outputType = method.outputType function( name = method.name, - modifiers = if (method.serverStreaming) "" else "suspend", - args = "message: ${inputType.name.safeFullName().wrapInFlowIf(method.clientStreaming)}", - returnType = outputType.name.safeFullName().wrapInFlowIf(method.serverStreaming), + modifiers = if (method.dec.isServerStreaming) "" else "suspend", + args = "message: ${inputType.name.safeFullName().wrapInFlowIf(method.dec.isClientStreaming)}", + returnType = outputType.name.safeFullName().wrapInFlowIf(method.dec.isServerStreaming), ) } } diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinJvmGenerator.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinJvmGenerator.kt deleted file mode 100644 index 48083b26e..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinJvmGenerator.kt +++ /dev/null @@ -1,695 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("detekt.all") - -package kotlinx.rpc.protobuf - -import kotlinx.rpc.protobuf.CodeGenerator.DeclarationType -import kotlinx.rpc.protobuf.model.* -import org.slf4j.Logger - -private const val RPC_INTERNAL_PACKAGE_SUFFIX = "_rpc_internal" - -class ModelToKotlinJvmGenerator( - private val model: Model, - private val logger: Logger, - private val codeGenerationParameters: CodeGenerationParameters, -) { - fun generateKotlinFiles(): List { - return model.files.flatMap { it.generateKotlinFiles() } - } - - private fun FileDeclaration.generateKotlinFiles(): List { - additionalPublicImports.clear() - additionalInternalImports.clear() - - return listOf( - generatePublicKotlinFile(), - generateInternalKotlinFile(), - ) - } - - private var currentPackage: FqName = FqName.Package.Root - - private fun FileDeclaration.generatePublicKotlinFile(): FileGenerator { - currentPackage = packageName - - return file(codeGenerationParameters, logger = logger) { - filename = this@generatePublicKotlinFile.name - packageName = this@generatePublicKotlinFile.packageName.safeFullName() - packagePath = this@generatePublicKotlinFile.packageName.safeFullName() - - dependencies.forEach { dependency -> - importPackage(dependency.packageName.safeFullName()) - } - - fileOptIns = listOf("ExperimentalRpcApi::class", "InternalRpcApi::class") - - generatePublicDeclaredEntities(this@generatePublicKotlinFile) - - import("kotlinx.rpc.internal.utils.*") - import("kotlinx.coroutines.flow.*") - - additionalPublicImports.forEach { - import(it) - } - } - } - - private fun FileDeclaration.generateInternalKotlinFile(): FileGenerator { - currentPackage = packageName - - return file(codeGenerationParameters, logger = logger) { - filename = this@generateInternalKotlinFile.name - packageName = this@generateInternalKotlinFile.packageName.safeFullName() - packagePath = - this@generateInternalKotlinFile.packageName.safeFullName() - .packageNameSuffixed(RPC_INTERNAL_PACKAGE_SUFFIX) - - fileOptIns = listOf("ExperimentalRpcApi::class", "InternalRpcApi::class") - - dependencies.forEach { dependency -> - importPackage(dependency.packageName.safeFullName()) - } - - generateInternalDeclaredEntities(this@generateInternalKotlinFile) - - import("kotlinx.rpc.internal.utils.*") - import("kotlinx.coroutines.flow.*") - - additionalInternalImports.forEach { - import(it) - } - } - } - - private val additionalPublicImports = mutableSetOf() - private val additionalInternalImports = mutableSetOf() - - private fun CodeGenerator.generatePublicDeclaredEntities(fileDeclaration: FileDeclaration) { - fileDeclaration.messageDeclarations.forEach { generatePublicMessage(it) } - fileDeclaration.enumDeclarations.forEach { generatePublicEnum(it) } - fileDeclaration.serviceDeclarations.forEach { generatePublicService(it) } - } - - private fun CodeGenerator.generateInternalDeclaredEntities(fileDeclaration: FileDeclaration) { - fileDeclaration.messageDeclarations.forEach { generateInternalMessage(it) } - fileDeclaration.serviceDeclarations.forEach { generateInternalService(it) } - - fileDeclaration.messageDeclarations.forEach { - generateToAndFromPlatformCastsRec(it) - } - - fileDeclaration.enumDeclarations.forEach { - generateToAndFromPlatformCastsEnum(it) - } - } - - private fun CodeGenerator.generateToAndFromPlatformCastsRec(declaration: MessageDeclaration) { - generateToAndFromPlatformCasts(declaration) - - declaration.nestedDeclarations.forEach { nested -> - generateToAndFromPlatformCastsRec(nested) - } - - declaration.enumDeclarations.forEach { nested -> - generateToAndFromPlatformCastsEnum(nested) - } - } - - private fun MessageDeclaration.fields() = actualFields.map { - it.transformToFieldDeclaration() to it - } - - @Suppress("detekt.CyclomaticComplexMethod") - private fun CodeGenerator.generatePublicMessage(declaration: MessageDeclaration) { - clazz( - name = declaration.name.simpleName, - declarationType = DeclarationType.Interface, - ) { - declaration.fields().forEach { (fieldDeclaration, _) -> - code("val $fieldDeclaration") - newLine() - } - - newLine() - - declaration.oneOfDeclarations.forEach { oneOf -> - generateOneOfPublic(oneOf) - } - - declaration.nestedDeclarations.forEach { nested -> - generatePublicMessage(nested) - } - - declaration.enumDeclarations.forEach { enum -> - generatePublicEnum(enum) - } - - clazz("", modifiers = "companion", declarationType = DeclarationType.Object) - } - } - - @Suppress("detekt.CyclomaticComplexMethod") - private fun CodeGenerator.generateInternalMessage(declaration: MessageDeclaration) { - clazz( - name = "${declaration.name.simpleName}Builder", - declarationType = DeclarationType.Class, - superTypes = listOf(declaration.name.safeFullName()), - ) { - declaration.fields().forEach { (fieldDeclaration, field) -> - val value = when { - field.type is FieldType.Reference && field.nullable -> { - "= null" - } - - field.type is FieldType.Reference -> { - additionalInternalImports.add("kotlin.properties.Delegates") - "by Delegates.notNull()" - } - - else -> { - "= ${field.type.defaultValue}" - } - } - - code("override var $fieldDeclaration $value") - newLine() - } - - declaration.nestedDeclarations.forEach { nested -> - generateInternalMessage(nested) - } - } - } - - private fun CodeGenerator.generateToAndFromPlatformCasts(declaration: MessageDeclaration) { - function( - name = "invoke", - modifiers = "operator", - args = "body: ${declaration.name.safeFullName("Builder")}.() -> Unit", - contextReceiver = "${declaration.name.safeFullName()}.Companion", - returnType = declaration.name.safeFullName(), - ) { - code("return ${declaration.name.safeFullName("Builder")}().apply(body)") - } - - val platformType = "${declaration.outerClassName.safeFullName()}.${declaration.name.fullNestedName()}" - function( - name = "toPlatform", - contextReceiver = declaration.name.safeFullName(), - returnType = platformType, - ) { - scope("return $platformType.newBuilder().apply", ".build()") { - declaration.actualFields.forEach { field -> - val setFieldCall = setFieldCall(field) - - when { - field.type is FieldType.OneOf -> { - scope("this@toPlatform.${field.name}?.let { value ->", openingBracket = false) { - scope("when (value)") { - val oneOf = declaration.oneOfDeclarations[field.type.index] - val oneOfName = oneOf.name.safeFullName() - - oneOf.variants.forEach { variant -> - code("is $oneOfName.${variant.name} -> ${setFieldCall(variant)}(value.value${variant.type.toPlatformCast()})") - } - } - } - } - - field.nullable -> { - code("this@toPlatform.${field.name}?.let { $setFieldCall(it${field.type.toPlatformCast()}) }") - } - - else -> { - code("$setFieldCall(this@toPlatform.${field.name}${field.type.toPlatformCast()})") - } - } - } - } - } - - function( - name = "toKotlin", - contextReceiver = platformType, - returnType = declaration.name.safeFullName(), - ) { - scope("return ${declaration.name.safeFullName()}") { - declaration.actualFields.forEach { field -> - val javaName = fieldJavaName(field) - - val getter = "this@toKotlin.$javaName${field.type.toKotlinCast()}" - when { - field.type is FieldType.OneOf -> { - val oneOf = declaration.oneOfDeclarations[field.type.index] - val oneOfName = oneOf.name.safeFullName() - - scope("${field.name} = when") { - oneOf.variants.forEach { variant -> - code( - "${hasFieldJavaMethod(variant)} -> $oneOfName.${variant.name}(this@toKotlin.${ - fieldJavaName( - variant - ) - }${variant.type.toKotlinCast()})" - ) - } - code("else -> null") - } - } - - field.nullable -> { - ifBranch( - prefix = "${field.name} = ", - condition = hasFieldJavaMethod(field), - ifBlock = { - code(getter) - }, - elseBlock = { - code("null") - } - ) - } - - else -> { - code("${field.name} = $getter") - } - } - } - } - } - } - - private fun fieldJavaName(field: FieldDeclaration): String { - val name = field.name.replaceFirstChar { ch -> ch.lowercase() } - return when (field.type) { - is FieldType.List -> "${name}List" - is FieldType.Map -> "${name}Map" - else -> name - } - } - - private fun hasFieldJavaMethod(field: FieldDeclaration): String = - "has${field.name.replaceFirstChar { ch -> ch.uppercase() }}()" - - private fun setFieldCall(field: FieldDeclaration): String { - val uppercaseName = field.name.replaceFirstChar { ch -> ch.uppercase() } - val setFieldCall = when (field.type) { - is FieldType.List -> "addAll$uppercaseName" - is FieldType.Map -> "putAll$uppercaseName" - else -> "set$uppercaseName" - } - return setFieldCall - } - - private fun FieldType.toPlatformCast(): String { - return when (this) { - FieldType.IntegralType.FIXED32 -> ".toInt()" - FieldType.IntegralType.FIXED64 -> ".toLong()" - FieldType.IntegralType.UINT32 -> ".toInt()" - FieldType.IntegralType.UINT64 -> ".toLong()" - FieldType.IntegralType.BYTES -> ".let { bytes -> com.google.protobuf.ByteString.copyFrom(bytes) }" - is FieldType.Reference -> ".toPlatform()".also { - val fq by value - importRootDeclarationIfNeeded(fq, "toPlatform", true) - } - - is FieldType.List -> ".map { it${value.toPlatformCast()} }" - - is FieldType.Map -> { - val entry by entry - - ".mapValues { it.value${entry.value.toPlatformCast()} }" - } - - else -> "" - } - } - - private fun FieldType.toKotlinCast(): String { - return when (this) { - FieldType.IntegralType.FIXED32 -> ".toUInt()" - FieldType.IntegralType.FIXED64 -> ".toULong()" - FieldType.IntegralType.UINT32 -> ".toUInt()" - FieldType.IntegralType.UINT64 -> ".toULong()" - FieldType.IntegralType.BYTES -> ".toByteArray()" - is FieldType.Reference -> ".toKotlin()".also { - val fq by value - importRootDeclarationIfNeeded(fq, "toKotlin", true) - } - - is FieldType.List -> ".map { it${value.toKotlinCast()} }" - - is FieldType.Map -> { - val entry by entry - - ".mapValues { it.value${entry.value.toKotlinCast()} }" - } - - else -> "" - } - } - - private fun FieldDeclaration.transformToFieldDeclaration(): String { - return "${name}: ${typeFqName()}" - } - - private fun FieldDeclaration.typeFqName(): String { - return when (type) { - is FieldType.Reference -> { - val value by type.value - value.safeFullName() - } - - is FieldType.OneOf -> { - val value by type.value - value.safeFullName() - } - - is FieldType.IntegralType -> { - type.fqName.simpleName - } - - is FieldType.List -> { - val fqValue = when (val value = type.value) { - is FieldType.Reference -> value.value.value - is FieldType.IntegralType -> value.fqName - else -> error("Unsupported type: $value") - } - - "List<${fqValue.safeFullName()}>" - } - - is FieldType.Map -> { - val entry by type.entry - - val fqKey = when (val key = entry.key) { - is FieldType.Reference -> key.value.value - is FieldType.IntegralType -> key.fqName - else -> error("Unsupported type: $key") - } - - val fqValue = when (val value = entry.value) { - is FieldType.Reference -> value.value.value - is FieldType.IntegralType -> value.fqName - else -> error("Unsupported type: $value") - } - - "Map<${fqKey.safeFullName()}, ${fqValue.safeFullName()}>" - } - }.withNullability(nullable) - } - - private fun String.withNullability(nullable: Boolean): String { - return "$this${if (nullable) "?" else ""}" - } - - private fun CodeGenerator.generateOneOfPublic(declaration: OneOfDeclaration) { - val interfaceName = declaration.name.simpleName - - clazz(interfaceName, "sealed", declarationType = DeclarationType.Interface) { - declaration.variants.forEach { variant -> - clazz( - name = variant.name, - modifiers = "value", - constructorArgs = listOf("val value: ${variant.typeFqName()}"), - annotations = listOf("@JvmInline"), - superTypes = listOf(interfaceName), - ) - - additionalPublicImports.add("kotlin.jvm.JvmInline") - } - } - } - - private fun CodeGenerator.generatePublicEnum(declaration: EnumDeclaration) { - clazz(declaration.name.simpleName, modifiers = "enum") { - declaration.originalEntries.forEach { entry -> - code("${entry.name.simpleName},") - newLine() - } - code(";") - newLine() - - if (declaration.aliases.isNotEmpty()) { - newLine() - - clazz("", modifiers = "companion", declarationType = DeclarationType.Object) { - declaration.aliases.forEach { alias: EnumDeclaration.Alias -> - code( - "val ${alias.name.simpleName}: ${declaration.name.simpleName} " + - "= ${alias.original.name.simpleName}" - ) - } - } - } - } - } - - private fun CodeGenerator.generateToAndFromPlatformCastsEnum(declaration: EnumDeclaration) { - val platformType = "${declaration.outerClassName.safeFullName()}.${declaration.name.fullNestedName()}" - - function( - name = "toPlatform", - contextReceiver = declaration.name.safeFullName(), - returnType = platformType, - ) { - scope("return when (this)") { - declaration.aliases.forEach { field -> - code("${declaration.name.fullNestedName()}.${field.name.simpleName} -> $platformType.${field.name.simpleName}") - } - - declaration.originalEntries.forEach { field -> - code("${declaration.name.fullNestedName()}.${field.name.simpleName} -> $platformType.${field.name.simpleName}") - } - } - } - - function( - name = "toKotlin", - contextReceiver = platformType, - returnType = declaration.name.safeFullName(), - ) { - scope("return when (this)") { - declaration.aliases.forEach { field -> - code("$platformType.${field.name.simpleName} -> ${declaration.name.fullNestedName()}.${field.name.simpleName}") - } - - declaration.originalEntries.forEach { field -> - code("$platformType.${field.name.simpleName} -> ${declaration.name.fullNestedName()}.${field.name.simpleName}") - } - } - } - } - - @Suppress("detekt.LongMethod") - private fun CodeGenerator.generatePublicService(service: ServiceDeclaration) { - code("@kotlinx.rpc.grpc.annotations.Grpc") - clazz(service.name.simpleName, declarationType = DeclarationType.Interface) { - service.methods.forEach { method -> - val inputType by method.inputType - val outputType by method.outputType - function( - name = method.name, - modifiers = if (method.serverStreaming) "" else "suspend", - args = "message: ${inputType.name.safeFullName().wrapInFlowIf(method.clientStreaming)}", - returnType = outputType.name.safeFullName().wrapInFlowIf(method.serverStreaming), - ) - } - } - } - - private fun String.wrapInFlowIf(condition: Boolean): String { - return if (condition) "Flow<$this>" else this - } - - private fun CodeGenerator.generateInternalService(service: ServiceDeclaration) { - code("@Suppress(\"unused\", \"all\")") - clazz( - modifiers = "private", - name = "${service.name.simpleName}Delegate", - declarationType = DeclarationType.Object, - superTypes = listOf("kotlinx.rpc.grpc.descriptor.GrpcDelegate<${service.name.safeFullName()}>"), - ) { - function( - name = "clientProvider", - modifiers = "override", - args = "channel: kotlinx.rpc.grpc.ManagedChannel", - returnType = "kotlinx.rpc.grpc.descriptor.GrpcClientDelegate", - ) { - code("return ${service.name.simpleName}ClientDelegate(channel)") - } - - function( - name = "definitionFor", - modifiers = "override", - args = "impl: ${service.name.safeFullName()}", - returnType = "kotlinx.rpc.grpc.ServerServiceDefinition", - ) { - scope("return ${service.name.simpleName}ServerDelegate(impl).bindService()") - } - } - - code("@Suppress(\"unused\", \"all\")") - clazz( - modifiers = "private", - name = "${service.name.simpleName}ServerDelegate", - declarationType = DeclarationType.Class, - superTypes = listOf("${service.name.simpleName}GrpcKt.${service.name.simpleName}CoroutineImplBase()"), - constructorArgs = listOf("private val impl: ${service.name.safeFullName()}"), - ) { - service.methods.forEach { method -> - val grpcName = method.name.replaceFirstChar { it.lowercase() } - - val inputType by method.inputType - val outputType by method.outputType - - function( - name = grpcName, - modifiers = "override${if (method.serverStreaming) "" else " suspend"}", - args = "request: ${inputType.toPlatformMessageType().wrapInFlowIf(method.clientStreaming)}", - returnType = outputType.toPlatformMessageType().wrapInFlowIf(method.serverStreaming), - ) { - val toKotlin = if (method.clientStreaming) { - "map { it.toKotlin() }" - } else { - "toKotlin()" - } - - val toPlatform = if (method.serverStreaming) { - "map { it.toPlatform() }" - } else { - "toPlatform()" - } - - code("return impl.${method.name}(request.${toKotlin}).${toPlatform}") - - importRootDeclarationIfNeeded(inputType.name, "toPlatform", true) - importRootDeclarationIfNeeded(outputType.name, "toKotlin", true) - } - } - } - - code("@Suppress(\"unused\", \"all\")") - clazz( - modifiers = "private", - name = "${service.name.simpleName}ClientDelegate", - declarationType = DeclarationType.Class, - superTypes = listOf("kotlinx.rpc.grpc.descriptor.GrpcClientDelegate"), - constructorArgs = listOf("private val channel: kotlinx.rpc.grpc.ManagedChannel"), - ) { - val stubType = "${service.name.simpleName}GrpcKt.${service.name.simpleName}CoroutineStub" - - property( - name = "stub", - modifiers = "private", - type = stubType, - delegate = true, - value = "lazy", - ) { - code("$stubType(channel.platformApi)") - } - - function( - name = "call", - modifiers = "override suspend", - args = "rpcCall: kotlinx.rpc.RpcCall", - typeParameters = "R", - returnType = "R", - ) { - val methods = service.methods.filter { !it.serverStreaming } - - if (methods.isEmpty()) { - code("error(\"Illegal call: \${rpcCall.callableName}\")") - return@function - } - - generateCallsImpls(methods) - } - - function( - name = "callServerStreaming", - modifiers = "override", - args = "rpcCall: kotlinx.rpc.RpcCall", - typeParameters = "R", - returnType = "Flow", - ) { - val methods = service.methods.filter { it.serverStreaming } - - if (methods.isEmpty()) { - code("error(\"Illegal streaming call: \${rpcCall.callableName}\")") - return@function - } - - generateCallsImpls(methods) - } - } - } - - private fun CodeGenerator.generateCallsImpls( - methods: List, - ) { - code("val message = rpcCall.parameters[0]") - code("@Suppress(\"UNCHECKED_CAST\")") - scope("return when (rpcCall.callableName)") { - methods.forEach { method -> - val inputType by method.inputType - val outputType by method.outputType - val grpcName = method.name.replaceFirstChar { it.lowercase() } - - val toKotlin = if (method.serverStreaming) { - "map { it.toKotlin() }" - } else { - "toKotlin()" - } - - val toPlatform = if (method.clientStreaming) { - "map { it.toPlatform() }" - } else { - "toPlatform()" - } - - val argumentCast = inputType.name.safeFullName().wrapInFlowIf(method.clientStreaming) - val resultCast = "R".wrapInFlowIf(method.serverStreaming) - - val result = "stub.$grpcName((message as $argumentCast).${toPlatform})" - code("\"${method.name}\" -> $result.${toKotlin} as $resultCast") - - importRootDeclarationIfNeeded(inputType.name, "toPlatform", true) - importRootDeclarationIfNeeded(outputType.name, "toKotlin", true) - } - - code("else -> error(\"Illegal call: \${rpcCall.callableName}\")") - } - } - - private fun MessageDeclaration.toPlatformMessageType(): String { - return "${outerClassName.safeFullName()}.${name.fullNestedName()}" - } - - private fun FqName.safeFullName(classSuffix: String = ""): String { - importRootDeclarationIfNeeded(this) - - return fullName(classSuffix) - } - - private fun importRootDeclarationIfNeeded( - declaration: FqName, - nameToImport: String = declaration.simpleName, - internalOnly: Boolean = false, - ) { - if (declaration.parent == FqName.Package.Root && currentPackage != FqName.Package.Root && nameToImport.isNotBlank()) { - additionalInternalImports.add(nameToImport) - if (!internalOnly) { - additionalPublicImports.add(nameToImport) - } - } - } -} - -private fun String.packageNameSuffixed(suffix: String): String { - return if (isEmpty()) suffix else "$this.$suffix" -} diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt deleted file mode 100644 index f101d1bd2..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -@file:Suppress("detekt.all") - -package kotlinx.rpc.protobuf - -import com.google.protobuf.DescriptorProtos -import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Type -import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest -import kotlinx.rpc.protobuf.model.* -import kotlinx.rpc.protobuf.model.FieldType.IntegralType -import org.slf4j.Logger -import kotlin.properties.Delegates - -private const val ENUM_UNRECOGNIZED = "UNRECOGNIZED" - -class ProtoToModelInterpreter( - @Suppress("unused") - private val logger: Logger, -) { - private val fileDependencies = mutableMapOf() - private val messages = mutableMapOf() - private val rootResolver = NameResolver.create(logger) - - fun interpretProtocRequest(message: CodeGeneratorRequest): Model { - val protoFileMap = message.protoFileList.associateBy { it.name } - - return Model(message.fileToGenerateList.map { protoFileMap.getValue(it).toModel() }) - } - - // package name of a currently parsed file - private var packageName: FqName.Package by Delegates.notNull() - - private fun DescriptorProtos.FileDescriptorProto.toModel(): FileDeclaration { - val dependencies = dependencyList.map { depFilename -> - fileDependencies[depFilename] - ?: error("Unknown dependency $depFilename for $name proto file, wrong topological order") - } - - packageName = FqName.Package.fromString(kotlinPackageName(`package`, options)) - - val resolver = rootResolver - .withScope(packageName) - .withImports(dependencies.map { it.packageName }) - - val outerClass = outerClassFq() - - return FileDeclaration( - name = kotlinFileName(), - packageName = packageName, - dependencies = dependencies, - messageDeclarations = messageTypeList.mapNotNull { it.toModel(resolver, outerClass, parent = null) }, - enumDeclarations = enumTypeList.map { it.toModel(resolver, outerClass, packageName) }, - serviceDeclarations = serviceList.map { it.toModel(resolver) }, - deprecated = options.deprecated, - doc = null, - ).also { - // TODO KRPC-144: check uniqueness - fileDependencies[name] = it - } - } - - private fun DescriptorProtos.FileDescriptorProto.outerClassFq(): FqName { - val filename = protoFileNameToKotlinName() - - val messageClash = messageTypeList.any { it.name.fullProtoNameToKotlin(firstLetterUpper = true) == filename } - val enumClash = enumTypeList.any { it.name.fullProtoNameToKotlin(firstLetterUpper = true) == filename } - val serviceClash = serviceList.any { it.name.fullProtoNameToKotlin(firstLetterUpper = true) == filename } - - val suffix = if (messageClash || enumClash || serviceClash) { - "OuterClass" - } else { - "" - } - - val simpleName = "${filename}$suffix" - return FqName.Declaration(simpleName, packageName) - } - - private fun DescriptorProtos.FileDescriptorProto.kotlinFileName(): String { - return "${protoFileNameToKotlinName()}.kt" - } - - private fun DescriptorProtos.FileDescriptorProto.protoFileNameToKotlinName(): String { - return name.removeSuffix(".proto").fullProtoNameToKotlin(firstLetterUpper = true) - } - - private fun kotlinPackageName(originalPackage: String, options: DescriptorProtos.FileOptions): String { - // todo check forbidden package names - return originalPackage - } - - private val mapEntries = mutableMapOf>() - - private fun DescriptorProtos.DescriptorProto.toModel( - parentResolver: NameResolver, - outerClass: FqName, - parent: FqName?, - ): MessageDeclaration? { - val simpleName = name.fullProtoNameToKotlin(firstLetterUpper = true) - val fqName = parentResolver.declarationFqName(simpleName, parent ?: packageName) - val resolver = parentResolver.withScope(fqName) - - if (options.mapEntry) { - mapEntries[fqName] = resolveMapEntry(resolver) - return null - } - - // resolve before fields - val nestedDeclarations = nestedTypeList.mapNotNull { it.toModel(resolver, outerClass, fqName) } - val fields = fieldList.mapNotNull { - val oneOfName = if (it.hasOneofIndex()) { - oneofDeclList[it.oneofIndex].name - } else { - null - } - - it.toModel(oneOfName, resolver) - } - - return MessageDeclaration( - outerClassName = outerClass, - name = fqName, - actualFields = fields, - oneOfDeclarations = oneofDeclList.mapIndexedNotNull { i, desc -> desc.toModel(i, resolver, fqName) }, - enumDeclarations = enumTypeList.map { it.toModel(resolver, outerClass, fqName) }, - nestedDeclarations = nestedDeclarations, - deprecated = options.deprecated, - doc = null, - ).apply { - messages[name] = this - } - } - - private fun DescriptorProtos.DescriptorProto.resolveMapEntry(resolver: NameResolver): Lazy = - lazy { - val keyType = fieldList[0].toModel(null, resolver) ?: error("Key type is null") - val valueType = fieldList[1].toModel(null, resolver) ?: error("Value type is null") - - FieldType.Map.Entry(keyType.type, valueType.type) - } - - private val oneOfFieldMembers = mutableMapOf>() - - private fun DescriptorProtos.FieldDescriptorProto.toModel( - oneOfName: String?, - resolver: NameResolver, - ): FieldDeclaration? { - if (oneOfName != null) { - val fieldType = when { - // effectively optional - // https://github.com/protocolbuffers/protobuf/blob/main/docs/implementing_proto3_presence.md#updating-a-code-generator - oneOfName == "_$name" -> { - fieldType(resolver) - } - - oneOfFieldMembers[oneofIndex] == null -> { - oneOfFieldMembers[oneofIndex] = mutableListOf() - .also { list -> list.add(this) } - - val name = oneOfName.fullProtoNameToKotlin(firstLetterUpper = true) - FieldType.OneOf(lazy { resolver.resolve(name) }, oneofIndex) - } - - else -> { - oneOfFieldMembers[oneofIndex]!!.add(this) - null - } - } ?: return null - - return FieldDeclaration( - name = oneOfName.removePrefix("_").fullProtoNameToKotlin(), - number = number, - type = fieldType, - nullable = true, - deprecated = options.deprecated, - doc = null, - proto = this, - ) - } - - return FieldDeclaration( - name = name.fullProtoNameToKotlin(), - number = number, - type = fieldType(resolver), - nullable = proto3Optional, - deprecated = options.deprecated, - doc = null, - proto = this, - ) - } - - private fun DescriptorProtos.FieldDescriptorProto.fieldType(resolver: NameResolver): FieldType { - return when { - // from https://github.com/protocolbuffers/protobuf/blob/5fc0e22b9f912c2aa94a34502887c3719e805834/src/google/protobuf/descriptor.proto#L294 - // if the name starts with a '.', it is fully-qualified. - hasTypeName() && typeName.startsWith(".") -> { - typeName - .substringAfter('.') - .fullProtoNameToKotlin(firstLetterUpper = true) - // KRPC-144 Import types - .let { resolvedType(it, resolver) } - } - - hasTypeName() -> { - typeName - .fullProtoNameToKotlin(firstLetterUpper = true) - // KRPC-144 Import types: we assume local types now - .let { resolvedType(it, resolver) } - } - - else -> { - wrapWithLabel(primitiveType()) - } - } - } - - @Suppress("detekt.CyclomaticComplexMethod") - private fun DescriptorProtos.FieldDescriptorProto.primitiveType(): IntegralType { - return when (type) { - Type.TYPE_STRING -> IntegralType.STRING - Type.TYPE_BYTES -> IntegralType.BYTES - Type.TYPE_BOOL -> IntegralType.BOOL - Type.TYPE_FLOAT -> IntegralType.FLOAT - Type.TYPE_DOUBLE -> IntegralType.DOUBLE - Type.TYPE_INT32 -> IntegralType.INT32 - Type.TYPE_INT64 -> IntegralType.INT64 - Type.TYPE_UINT32 -> IntegralType.UINT32 - Type.TYPE_UINT64 -> IntegralType.UINT64 - Type.TYPE_FIXED32 -> IntegralType.FIXED32 - Type.TYPE_FIXED64 -> IntegralType.FIXED64 - Type.TYPE_SINT32 -> IntegralType.SINT32 - Type.TYPE_SINT64 -> IntegralType.SINT64 - Type.TYPE_SFIXED32 -> IntegralType.SFIXED32 - Type.TYPE_SFIXED64 -> IntegralType.SFIXED64 - - Type.TYPE_ENUM, Type.TYPE_MESSAGE, Type.TYPE_GROUP, null -> - error("Expected to find primitive type, instead got $type with name '$typeName'") - } - } - - private fun DescriptorProtos.FieldDescriptorProto.resolvedType(name: String, resolver: NameResolver): FieldType { - val entry = mapEntries[resolver.resolveOrNull(name)] - - if (entry != null) { - return FieldType.Map(entry) - } - - val fieldType = FieldType.Reference(lazy { resolver.resolve(name) }) - - return wrapWithLabel(fieldType) - } - - private fun DescriptorProtos.FieldDescriptorProto.wrapWithLabel(fieldType: FieldType): FieldType { - return when (label) { - DescriptorProtos.FieldDescriptorProto.Label.LABEL_REPEATED -> { - FieldType.List(fieldType) - } - // LABEL_OPTIONAL is not actually optional in proto3. - // Actual optional is oneOf with one option and same name - else -> { - fieldType - } - } - } - - private fun DescriptorProtos.OneofDescriptorProto.toModel( - index: Int, - resolver: NameResolver, - parent: FqName, - ): OneOfDeclaration? { - val name = name.fullProtoNameToKotlin(firstLetterUpper = true) - val fqName = resolver.declarationFqName(name, parent) - - val fields = oneOfFieldMembers[index] ?: return null - - val fieldResolver = resolver.withScope(fqName) - return OneOfDeclaration( - name = fqName, - variants = fields.map { field -> - FieldDeclaration( - name = field.name.fullProtoNameToKotlin(firstLetterUpper = true), - number = field.number, - type = field.fieldType(fieldResolver), - nullable = false, - deprecated = field.options.deprecated, - doc = null, - proto = field, - ) - } - ) - } - - private fun DescriptorProtos.EnumDescriptorProto.toModel( - resolver: NameResolver, - outerClassName: FqName, - parent: FqName, - ): EnumDeclaration { - val allowAlias = options.allowAlias - val originalEntries = mutableMapOf() - val aliases = mutableListOf() - - val enumName = resolver.declarationFqName(name.fullProtoNameToKotlin(firstLetterUpper = true), parent) - - valueList.forEach { enumEntry -> - val original = originalEntries[enumEntry.number] - if (original != null) { - if (!allowAlias) { - error( - "Aliases are not allowed for enum type $name: " + - "${enumEntry.number} of ${enumEntry.name} is already used by $original entry. " + - "Allow aliases via `allow_alias = true` option to avoid this error." - ) - } - - aliases.add( - EnumDeclaration.Alias( - name = resolver.declarationFqName(enumEntry.name, enumName), - original = original, - deprecated = enumEntry.options.deprecated, - doc = null, - ) - ) - } else { - originalEntries[enumEntry.number] = EnumDeclaration.Entry( - name = resolver.declarationFqName(enumEntry.name, enumName), - deprecated = enumEntry.options.deprecated, - doc = null, - ) - } - } - - originalEntries[-1] = EnumDeclaration.Entry( - name = resolver.declarationFqName(ENUM_UNRECOGNIZED, enumName), - deprecated = false, - doc = null, - ) - - return EnumDeclaration( - name = enumName, - outerClassName = outerClassName, - originalEntries = originalEntries.values.toList(), - aliases = aliases, - deprecated = options.deprecated, - doc = null, - ) - } - - private fun DescriptorProtos.ServiceDescriptorProto.toModel(resolver: NameResolver): ServiceDeclaration { - return ServiceDeclaration( - name = resolver.declarationFqName(name.fullProtoNameToKotlin(firstLetterUpper = true), packageName), - methods = methodList.map { it.toModel(resolver) } - ) - } - - private fun DescriptorProtos.MethodDescriptorProto.toModel(resolver: NameResolver): MethodDeclaration { - return MethodDeclaration( - name = name.fullProtoNameToKotlin(firstLetterUpper = false), - inputType = inputType - .substringAfter('.') // see typeName resolution - .fullProtoNameToKotlin(firstLetterUpper = true) - .let { - lazy { - messages[resolver.resolve(it)] - ?: error("Unknown message type $it, available: ${messages.keys.joinToString(",")}") - } - }, - outputType = outputType - .substringAfter('.') // see typeName resolution - .fullProtoNameToKotlin(firstLetterUpper = true) - .let { - lazy { - messages[resolver.resolve(it)] - ?: error("Unknown message type $it, available: ${messages.keys.joinToString(",")}") - } - }, - clientStreaming = clientStreaming, - serverStreaming = serverStreaming, - ) - } - - private fun String.fullProtoNameToKotlin(firstLetterUpper: Boolean = false): String { - val lastDelimiterIndex = indexOfLast { it == '.' || it == '/' } - return if (lastDelimiterIndex != -1) { - val packageName = substring(0, lastDelimiterIndex) - val name = substring(lastDelimiterIndex + 1) - val delimiter = this[lastDelimiterIndex] - return "$packageName$delimiter${name.simpleProtoNameToKotlin(firstLetterUpper = true)}" - } else { - simpleProtoNameToKotlin(firstLetterUpper) - } - } - - private val snakeRegExp = "(_[a-z]|-[a-z])".toRegex() - - private fun String.snakeToCamelCase(): String { - return replace(snakeRegExp) { it.value.last().uppercase() } - } - - private fun String.simpleProtoNameToKotlin(firstLetterUpper: Boolean = false): String { - return snakeToCamelCase().run { - if (firstLetterUpper) { - replaceFirstChar { it.uppercase() } - } else { - this - } - } - } - - private fun NameResolver.declarationFqName(simpleName: String, parent: FqName): FqName.Declaration { - return FqName.Declaration(simpleName, parent).also { add(it) } - } -} diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt index 9117e317d..4ff31adbf 100644 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt +++ b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt @@ -20,30 +20,9 @@ import java.io.File class RpcProtobufPlugin { companion object { private const val DEBUG_OUTPUT_OPTION = "debugOutput" - private const val MESSAGE_MODE_OPTION = "messageMode" - - // if set to "common" we generate kotlin common source code - private const val TARGET_MODE_OPTION = "targetMode" - } - - enum class MessageMode { - Interface, Class; - - companion object { - fun of(value: String?): MessageMode { - return when (value) { - "interface" -> Interface - "class" -> Class - null -> error("Message mode is not specified, use --messageMode=interface or --messageMode=class") - else -> error("Unknown message mode: $value") - } - } - } } private var debugOutput: String? = null - private lateinit var messageGenerationMode: MessageMode - private var targetCommon: Boolean = false private val logger: Logger by lazy { val debugOutput = debugOutput ?: return@lazy NOPLogger.NOP_LOGGER @@ -74,10 +53,8 @@ class RpcProtobufPlugin { } debugOutput = parameters[DEBUG_OUTPUT_OPTION] - messageGenerationMode = MessageMode.of(parameters[MESSAGE_MODE_OPTION]) - targetCommon = parameters[TARGET_MODE_OPTION] == "common" - val files = input.generateKotlinFiles() + val files = input.generateKotlinCommonFiles() .map { file -> CodeGeneratorResponse.File.newBuilder() .apply { @@ -104,19 +81,10 @@ class RpcProtobufPlugin { .build() } - private fun CodeGeneratorRequest.generateKotlinFiles(): List { - val interpreter = ProtoToModelInterpreter(logger) - val model = interpreter.interpretProtocRequest(this) - - // choose common generator if targetMode option was set. - if (targetCommon) { - val fileGenerator = - ModelToKotlinCommonGenerator(model, logger, CodeGenerationParameters(messageGenerationMode)) - return fileGenerator.generateKotlinFiles() - } else { - val fileGenerator = - ModelToKotlinJvmGenerator(model, logger, CodeGenerationParameters(messageGenerationMode)) - return fileGenerator.generateKotlinFiles() - } + private fun CodeGeneratorRequest.generateKotlinCommonFiles(): List { + val model = this.toModel() + val fileGenerator = ModelToKotlinCommonGenerator(model, logger) + return fileGenerator.generateKotlinFiles() } } + diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/codeRequestToModel.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/codeRequestToModel.kt new file mode 100644 index 000000000..3f1702701 --- /dev/null +++ b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/codeRequestToModel.kt @@ -0,0 +1,285 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import com.google.protobuf.DescriptorProtos +import com.google.protobuf.Descriptors +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest +import kotlinx.rpc.protobuf.model.* + +private val modelCache = mutableMapOf() + +/** + * Converts a [CodeGeneratorRequest] into the protoc plugin [Model] of the protobuf. + * + * @return a [Model] instance containing a list of [FileDeclaration]s that represent + * the converted protobuf files. + */ +fun CodeGeneratorRequest.toModel(): Model { + val protoFileMap = protoFileList.associateBy { it.name } + val fileDescriptors = mutableMapOf() + + val files = fileToGenerateList.map { protoFileMap[it]!! } + .map { protoFile -> protoFile.toDescriptor(protoFileMap, fileDescriptors) } + + return Model( + files = files.map { it.toModel() } + ) +} + + +/** + * Converts a [DescriptorProtos.FileDescriptorProto] instance to a [Descriptors.FileDescriptor], + * resolving its dependencies. + * + * @param protoFileMap a map of file names to `FileDescriptorProto` objects which are available for resolution. + * @param cache a mutable map that stores already resolved `FileDescriptor` objects by file name to prevent + * redundant computations. + * @return the resolved `FileDescriptor` instance corresponding to this `FileDescriptorProto`. + */ +private fun DescriptorProtos.FileDescriptorProto.toDescriptor( + protoFileMap: Map, + cache: MutableMap +): Descriptors.FileDescriptor { + if (cache.containsKey(name)) return cache[name]!! + + val dependencies = dependencyList.map { depName -> + val depProto = protoFileMap[depName] ?: error("Missing dependency: $depName") + depProto.toDescriptor(protoFileMap, cache) + }.toTypedArray() + + val fileDescriptor = Descriptors.FileDescriptor.buildFrom(this, dependencies) + cache[name] = fileDescriptor + return fileDescriptor +} + +/** + * Returns the fully qualified name [FqName] of this descriptor, resolving it to the most specific + * declaration or package name based on its type. + * + * Depending on the type of the descriptor, the fully qualified name is computed recursively, + * using the containing type or file, and appropriately converting names. + * + * @return The fully qualified name represented as an instance of FqName, specific to the descriptor's context. + */ +private fun Descriptors.GenericDescriptor.fqName(): FqName { + val nameCapital = name.simpleProtoNameToKotlin(firstLetterUpper = true) + val nameLower = name.simpleProtoNameToKotlin() + return when (this) { + is Descriptors.FileDescriptor -> FqName.Package.fromString(`package`) + is Descriptors.Descriptor -> FqName.Declaration(nameCapital, containingType?.fqName() ?: file.fqName()) + is Descriptors.FieldDescriptor -> { + val usedName = if (realContainingOneof != null) nameCapital else nameLower + FqName.Declaration(usedName, containingType?.fqName() ?: file.fqName()) + } + + is Descriptors.EnumValueDescriptor -> FqName.Declaration(name, type.fqName()) + is Descriptors.OneofDescriptor -> FqName.Declaration(nameCapital, containingType?.fqName() ?: file.fqName()) + is Descriptors.ServiceDescriptor -> FqName.Declaration(nameCapital, file?.fqName() ?: file.fqName()) + is Descriptors.MethodDescriptor -> FqName.Declaration(nameLower, service?.fqName() ?: file.fqName()) + else -> error("Unknown generic descriptor: $this") + } +} + +/** + * Caches the `descriptor.toModel()` result in the [modelCache] to ensure that only a single object + * per descriptor exists. + */ +private inline fun D.cached(block: (D) -> T): T + where D : Descriptors.GenericDescriptor, T : Any { + if (modelCache.containsKey(this)) { + return modelCache[this] as T + } + val declaration = block(this) + modelCache[this] = declaration + return declaration +} + +private fun Descriptors.FileDescriptor.toModel(): FileDeclaration = cached { + return FileDeclaration( + name = kotlinFileName(), + packageName = FqName.Package.fromString(`package`), + dependencies = dependencies.map { it.toModel() }, + messageDeclarations = messageTypes.map { it.toModel() }, + enumDeclarations = enumTypes.map { it.toModel() }, + serviceDeclarations = services.map { it.toModel() }, + doc = null, + dec = this, + ) +} + +private fun Descriptors.Descriptor.toModel(): MessageDeclaration = cached { + val regularFields = fields.filter { field -> field.realContainingOneof == null }.map { it.toModel() } + + return MessageDeclaration( + name = fqName(), + actualFields = regularFields, + // get all oneof declarations that are not created from an optional in proto3 https://github.com/googleapis/api-linter/issues/1323 + oneOfDeclarations = oneofs.filter { it.fields[0].realContainingOneof != null }.map { it.toModel() }, + enumDeclarations = enumTypes.map { it.toModel() }, + nestedDeclarations = nestedTypes.map { it.toModel() }, + doc = null, + dec = this, + ) +} + +private fun Descriptors.FieldDescriptor.toModel(): FieldDeclaration = cached { + toProto().hasProto3Optional() + return FieldDeclaration( + name = fqName().simpleName, + type = modelType(), + doc = null, + dec = this, + ) +} + + +private fun Descriptors.OneofDescriptor.toModel(): OneOfDeclaration = cached { + return OneOfDeclaration( + name = fqName(), + variants = fields.map { it.toModel() }, + dec = this, + ) +} + +private fun Descriptors.EnumDescriptor.toModel(): EnumDeclaration = cached { + val entriesMap = mutableMapOf() + val aliases = mutableListOf() + + values.forEach { value -> + if (entriesMap.containsKey(value.number)) { + val original = entriesMap.getValue(value.number) + aliases.add(value.toAliasModel(original)) + } else { + entriesMap[value.number] = value.toModel() + } + } + + if (!options.allowAlias && aliases.isNotEmpty()) { + error("Enum ${fullName} has aliases: ${aliases.joinToString { it.name.simpleName }}") + } + + return EnumDeclaration( + name = fqName(), + originalEntries = entriesMap.values.toList(), + aliases = aliases, + doc = null, + dec = this, + ) +} + +private fun Descriptors.EnumValueDescriptor.toModel(): EnumDeclaration.Entry = cached { + return EnumDeclaration.Entry( + name = fqName(), + doc = null, + dec = this, + ) +} + +// no caching, as it would conflict with .toModel +private fun Descriptors.EnumValueDescriptor.toAliasModel(original: EnumDeclaration.Entry): EnumDeclaration.Alias { + return EnumDeclaration.Alias( + name = fqName(), + original = original, + doc = null, + dec = this, + ) +} + +private fun Descriptors.ServiceDescriptor.toModel(): ServiceDeclaration = cached { + return ServiceDeclaration( + name = fqName(), + methods = methods.map { it.toModel() }, + dec = this, + ) +} + +private fun Descriptors.MethodDescriptor.toModel(): MethodDeclaration = cached { + return MethodDeclaration( + name = name, + inputType = inputType.toModel(), + outputType = outputType.toModel(), + dec = this, + ) +} + +//// Type Conversion Extension //// + +private fun Descriptors.FieldDescriptor.modelType(): FieldType { + val baseType = when (type) { + Descriptors.FieldDescriptor.Type.DOUBLE -> FieldType.IntegralType.DOUBLE + Descriptors.FieldDescriptor.Type.FLOAT -> FieldType.IntegralType.FLOAT + Descriptors.FieldDescriptor.Type.INT64 -> FieldType.IntegralType.INT64 + Descriptors.FieldDescriptor.Type.UINT64 -> FieldType.IntegralType.UINT64 + Descriptors.FieldDescriptor.Type.INT32 -> FieldType.IntegralType.INT32 + Descriptors.FieldDescriptor.Type.FIXED64 -> FieldType.IntegralType.FIXED64 + Descriptors.FieldDescriptor.Type.FIXED32 -> FieldType.IntegralType.FIXED32 + Descriptors.FieldDescriptor.Type.BOOL -> FieldType.IntegralType.BOOL + Descriptors.FieldDescriptor.Type.STRING -> FieldType.IntegralType.STRING + Descriptors.FieldDescriptor.Type.BYTES -> FieldType.IntegralType.BYTES + Descriptors.FieldDescriptor.Type.UINT32 -> FieldType.IntegralType.UINT32 + Descriptors.FieldDescriptor.Type.SFIXED32 -> FieldType.IntegralType.SFIXED32 + Descriptors.FieldDescriptor.Type.SFIXED64 -> FieldType.IntegralType.SFIXED64 + Descriptors.FieldDescriptor.Type.SINT32 -> FieldType.IntegralType.SINT32 + Descriptors.FieldDescriptor.Type.SINT64 -> FieldType.IntegralType.SINT64 + Descriptors.FieldDescriptor.Type.ENUM -> FieldType.Reference(lazy { enumType!!.toModel().name }) + Descriptors.FieldDescriptor.Type.MESSAGE -> FieldType.Reference(lazy { messageType!!.toModel().name }) + Descriptors.FieldDescriptor.Type.GROUP -> error("GROUP type is unsupported") + } + + if (isMapField) { + val keyType = messageType.findFieldByName("key").modelType() + val valType = messageType.findFieldByName("value").modelType() + val mapEntry = FieldType.Map.Entry(keyType, valType) + return FieldType.Map(lazy { mapEntry }) + } + + if (isRepeated) { + return FieldType.List(baseType) + } + + return baseType +} + +//// Utility Extensions //// + +private fun Descriptors.FileDescriptor.kotlinFileName(): String { + return "${protoFileNameToKotlinName()}.kt" +} + +private fun Descriptors.FileDescriptor.protoFileNameToKotlinName(): String { + return name.removeSuffix(".proto").fullProtoNameToKotlin(firstLetterUpper = true) +} + + +private fun String.fullProtoNameToKotlin(firstLetterUpper: Boolean = false): String { + val lastDelimiterIndex = indexOfLast { it == '.' || it == '/' } + return if (lastDelimiterIndex != -1) { + val packageName = substring(0, lastDelimiterIndex) + val name = substring(lastDelimiterIndex + 1) + val delimiter = this[lastDelimiterIndex] + return "$packageName$delimiter${name.simpleProtoNameToKotlin(firstLetterUpper = true)}" + } else { + simpleProtoNameToKotlin(firstLetterUpper) + } +} + + +private val snakeRegExp = "(_[a-z]|-[a-z])".toRegex() + +private fun String.snakeToCamelCase(): String { + return replace(snakeRegExp) { it.value.last().uppercase() } +} + +private fun String.simpleProtoNameToKotlin(firstLetterUpper: Boolean = false): String { + return snakeToCamelCase().run { + if (firstLetterUpper) { + replaceFirstChar { it.uppercase() } + } else { + this + } + } +} + diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt deleted file mode 100644 index 89ede3e25..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.protobuf.model - -data class EnumDeclaration( - val outerClassName: FqName, - val name: FqName, - val originalEntries: List, - val aliases: List, - val deprecated: Boolean, - val doc: String?, -) { - data class Entry( - val name: FqName, - val deprecated: Boolean, - val doc: String?, - ) - - data class Alias( - val name: FqName, - val original: Entry, - val deprecated: Boolean, - val doc: String?, - ) -} diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldType.kt similarity index 70% rename from protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt rename to protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldType.kt index f76f321a9..50f50846f 100644 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt +++ b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldType.kt @@ -4,34 +4,6 @@ package kotlinx.rpc.protobuf.model -import com.google.protobuf.DescriptorProtos - -data class FieldDeclaration( - val name: String, - val number: Int, - val type: FieldType, - val nullable: Boolean, - val deprecated: Boolean, - val doc: String?, - val proto: DescriptorProtos.FieldDescriptorProto -) { - val isRepeated = proto.label == DescriptorProtos.FieldDescriptorProto.Label.LABEL_REPEATED - val isExtension = proto.hasExtendee() - val containsOneOf = proto.hasOneofIndex() - - val hasPresence = if (isRepeated) false else - proto.proto3Optional || proto.type == DescriptorProtos.FieldDescriptorProto.Type.TYPE_MESSAGE - || proto.type == DescriptorProtos.FieldDescriptorProto.Type.TYPE_GROUP - || isExtension || containsOneOf - - val isPackable = isRepeated && type.isPackable - - val packed = isPackable // TODO: must checked if this is also declared as [packed = true] (or proto3 auto packed) - - val packedFixedSize = type.wireType == WireType.FIXED64 || type.wireType == WireType.FIXED32 -} - - enum class WireType { VARINT, FIXED64, diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt deleted file mode 100644 index 843817361..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.protobuf.model - -data class FileDeclaration( - val name: String, - val packageName: FqName.Package, - val dependencies: List, - val messageDeclarations: List, - val enumDeclarations: List, - val serviceDeclarations: List, - val deprecated: Boolean, - val doc: String?, -) diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt deleted file mode 100644 index 675c982a9..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.protobuf.model - -data class MessageDeclaration( - val outerClassName: FqName, - val name: FqName, - val actualFields: List, // excludes oneOf fields, but includes oneOf itself - val oneOfDeclarations: List, - val enumDeclarations: List, - val nestedDeclarations: List, - val deprecated: Boolean, - val doc: String?, -) diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt deleted file mode 100644 index 1c3527b76..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.protobuf.model - -data class MethodDeclaration( - val name: String, - val clientStreaming: Boolean, - val serverStreaming: Boolean, - val inputType: Lazy, - val outputType: Lazy, -) diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt deleted file mode 100644 index 5cb43129a..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.protobuf.model - -data class Model( - val files: List, -) diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/NameResolver.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/NameResolver.kt index 8f469b293..fb136714a 100644 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/NameResolver.kt +++ b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/NameResolver.kt @@ -2,192 +2,196 @@ * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -package kotlinx.rpc.protobuf.model - -import org.slf4j.Logger - -internal class NameResolver private constructor( - private val logger: Logger, - private val root: Node = Node(FqName.Package.Root.simpleName, FqName.Package.Root), - private val scope: Node = root, - private val imports: List = emptyList(), -) { - fun add(name: FqName) { - val parentNode = getParentNodeFor(name) - - if (parentNode.children.containsKey(name.simpleName)) { - error("Name ${name.fullName()} already exists") - } - - parentNode.children[name.simpleName] = Node(name.simpleName, name, parentNode) - } - - private fun getParentNodeFor(name: FqName): Node { - var next: FqName = name - val parents = mutableListOf() - while (next.parent != next) { - next = next.parent - if (next != next.parent) { - parents += next - } - } - - var parentNode = root - parents.reversed().forEach { parent -> - parentNode = parentNode.resolve(parent) - } - return parentNode - } - - private fun Node.resolve(name: FqName): Node { - if (name is FqName.Declaration && !children.containsKey(name.simpleName)) { - error("Name ${name.fullName()} doesn't exist, can't resolve ${name.fullName()}") - } - - return children.getOrPut(name.simpleName) { - Node(name.simpleName, name, this) - } - } - - fun resolve(name: String): FqName { - return resolveOrNull(name) ?: error("Name $name doesn't exist") - } - - fun resolveOrNull(name: String): FqName? { - val (parents, simpleName) = name.asParentsAndSimpleName() - - return resolveInScope(parents, simpleName, scope) - ?: run { - for (import in imports) { - resolveInScope(parents, simpleName, import) - ?.let { return it } - } - - null - } - } - - private fun resolveInScope(parents: List, simpleName: String, scope: Node): FqName? { - val inScope = resolveFromNode(scope, parents, simpleName) - - return when (inScope) { - is ResolveResult.Success -> { - inScope.fqName - } - - is ResolveResult.PartialResolve -> { - null - } - - is ResolveResult.NoResolve -> { - val inRoot = resolveFromNode(root, parents, simpleName) - - when (inRoot) { - is ResolveResult.Success -> { - inRoot.fqName - } - - else -> { - var node = scope.parent - while (node != null && node.fqName is FqName.Declaration) { - val inImport = resolveFromNode(node, parents, simpleName) - - if (inImport is ResolveResult.Success) { - return inImport.fqName - } - - node = node.parent - } - - null - } - } - } - } - } - - private fun resolveFromNode(start: Node, parents: List, simpleName: String): ResolveResult { - val declarationOnlyResolve = start.fqName != FqName.Package.Root && start.fqName is FqName.Package - var node: Node? = start - var i = 0 - var entered = false - while (i != parents.size) { - node = node?.children[parents[i++]] - if (node == null) { - break - } else if (node.fqName is FqName.Package && declarationOnlyResolve) { - return ResolveResult.NoResolve - } - entered = true - } - - if (node != null) { - val name = node.children[simpleName] - if (name != null) { - return ResolveResult.Success(name.fqName) - } - } - - return if (entered) { - ResolveResult.PartialResolve - } else { - ResolveResult.NoResolve - } - } - - sealed interface ResolveResult { - data class Success(val fqName: FqName) : ResolveResult - data object PartialResolve : ResolveResult - data object NoResolve : ResolveResult - } - - fun withImports(imports: List): NameResolver { - return NameResolver(logger, root, scope, imports.map { getParentNodeFor(it).resolve(it) }) - } - - fun withScope(name: FqName): NameResolver { - val node = getParentNodeFor(name).resolve(name) - return NameResolver(logger, root, node, imports) - } - - companion object { - fun create(logger: Logger): NameResolver { - return NameResolver(logger) - } - } - - private class Node( - val name: String, - val fqName: FqName, - val parent: Node? = null, - val children: MutableMap = mutableMapOf(), - ) { - private var _list: List? = null - - fun asList(): List { - if (_list != null) { - return _list!! - } - - if (parent == null) { - return emptyList() - } - - _list = parent.asList() + name - return _list!! - } - } - - @Suppress("unused") - fun pprint(): String { - return buildString { pprint(root, 0) } - } - - private fun StringBuilder.pprint(node: Node, indent: Int) { - val spaces = " ".repeat(indent) - appendLine("$spaces${node.fqName.fullName()}") - for (child in node.children.values) { - pprint(child, indent + 4) - } - } -} +// The NameResolver isn't used as we rely on the Java Descriptor for message resolving. +// However, it might be useful if we write a bootstrapped plugin in K/N. +// +//package kotlinx.rpc.protobuf.model +// +//import org.slf4j.Logger +// +// +//internal class NameResolver private constructor( +// private val logger: Logger, +// private val root: Node = Node(FqName.Package.Root.simpleName, FqName.Package.Root), +// private val scope: Node = root, +// private val imports: List = emptyList(), +//) { +// fun add(name: FqName) { +// val parentNode = getParentNodeFor(name) +// +// if (parentNode.children.containsKey(name.simpleName)) { +// error("Name ${name.fullName()} already exists") +// } +// +// parentNode.children[name.simpleName] = Node(name.simpleName, name, parentNode) +// } +// +// private fun getParentNodeFor(name: FqName): Node { +// var next: FqName = name +// val parents = mutableListOf() +// while (next.parent != next) { +// next = next.parent +// if (next != next.parent) { +// parents += next +// } +// } +// +// var parentNode = root +// parents.reversed().forEach { parent -> +// parentNode = parentNode.resolve(parent) +// } +// return parentNode +// } +// +// private fun Node.resolve(name: FqName): Node { +// if (name is FqName.Declaration && !children.containsKey(name.simpleName)) { +// error("Name ${name.fullName()} doesn't exist, can't resolve ${name.fullName()}") +// } +// +// return children.getOrPut(name.simpleName) { +// Node(name.simpleName, name, this) +// } +// } +// +// fun resolve(name: String): FqName { +// return resolveOrNull(name) ?: error("Name $name doesn't exist") +// } +// +// fun resolveOrNull(name: String): FqName? { +// val (parents, simpleName) = name.asParentsAndSimpleName() +// +// return resolveInScope(parents, simpleName, scope) +// ?: run { +// for (import in imports) { +// resolveInScope(parents, simpleName, import) +// ?.let { return it } +// } +// +// null +// } +// } +// +// private fun resolveInScope(parents: List, simpleName: String, scope: Node): FqName? { +// val inScope = resolveFromNode(scope, parents, simpleName) +// +// return when (inScope) { +// is ResolveResult.Success -> { +// inScope.fqName +// } +// +// is ResolveResult.PartialResolve -> { +// null +// } +// +// is ResolveResult.NoResolve -> { +// val inRoot = resolveFromNode(root, parents, simpleName) +// +// when (inRoot) { +// is ResolveResult.Success -> { +// inRoot.fqName +// } +// +// else -> { +// var node = scope.parent +// while (node != null && node.fqName is FqName.Declaration) { +// val inImport = resolveFromNode(node, parents, simpleName) +// +// if (inImport is ResolveResult.Success) { +// return inImport.fqName +// } +// +// node = node.parent +// } +// +// null +// } +// } +// } +// } +// } +// +// private fun resolveFromNode(start: Node, parents: List, simpleName: String): ResolveResult { +// val declarationOnlyResolve = start.fqName != FqName.Package.Root && start.fqName is FqName.Package +// var node: Node? = start +// var i = 0 +// var entered = false +// while (i != parents.size) { +// node = node?.children[parents[i++]] +// if (node == null) { +// break +// } else if (node.fqName is FqName.Package && declarationOnlyResolve) { +// return ResolveResult.NoResolve +// } +// entered = true +// } +// +// if (node != null) { +// val name = node.children[simpleName] +// if (name != null) { +// return ResolveResult.Success(name.fqName) +// } +// } +// +// return if (entered) { +// ResolveResult.PartialResolve +// } else { +// ResolveResult.NoResolve +// } +// } +// +// sealed interface ResolveResult { +// data class Success(val fqName: FqName) : ResolveResult +// data object PartialResolve : ResolveResult +// data object NoResolve : ResolveResult +// } +// +// fun withImports(imports: List): NameResolver { +// return NameResolver(logger, root, scope, imports.map { getParentNodeFor(it).resolve(it) }) +// } +// +// fun withScope(name: FqName): NameResolver { +// val node = getParentNodeFor(name).resolve(name) +// return NameResolver(logger, root, node, imports) +// } +// +// companion object { +// fun create(logger: Logger): NameResolver { +// return NameResolver(logger) +// } +// } +// +// private class Node( +// val name: String, +// val fqName: FqName, +// val parent: Node? = null, +// val children: MutableMap = mutableMapOf(), +// ) { +// private var _list: List? = null +// +// fun asList(): List { +// if (_list != null) { +// return _list!! +// } +// +// if (parent == null) { +// return emptyList() +// } +// +// _list = parent.asList() + name +// return _list!! +// } +// } +// +// @Suppress("unused") +// fun pprint(): String { +// return buildString { pprint(root, 0) } +// } +// +// private fun StringBuilder.pprint(node: Node, indent: Int) { +// val spaces = " ".repeat(indent) +// appendLine("$spaces${node.fqName.fullName()}") +// for (child in node.children.values) { +// pprint(child, indent + 4) +// } +// } +//} \ No newline at end of file diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt deleted file mode 100644 index aab8d237f..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.protobuf.model - -data class OneOfDeclaration( - val name: FqName, - val variants: List, -) diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt deleted file mode 100644 index 54e635d62..000000000 --- a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.rpc.protobuf.model - -data class ServiceDeclaration( - val name: FqName, - val methods: List, -) diff --git a/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/model.kt b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/model.kt new file mode 100644 index 000000000..57267380a --- /dev/null +++ b/protoc-gen/src/main/kotlin/kotlinx/rpc/protobuf/model/model.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +import com.google.protobuf.Descriptors + +data class Model( + val files: List, +) + +data class FileDeclaration( + val name: String, + val packageName: FqName.Package, + val dependencies: List, + val messageDeclarations: List, + val enumDeclarations: List, + val serviceDeclarations: List, + val doc: String?, + val dec: Descriptors.FileDescriptor, +) + +data class MessageDeclaration( + val name: FqName, + val actualFields: List, // excludes oneOf fields, but includes oneOf itself + val oneOfDeclarations: List, + val enumDeclarations: List, + val nestedDeclarations: List, + val doc: String?, + val dec: Descriptors.Descriptor, +) + +data class EnumDeclaration( + val name: FqName, + val originalEntries: List, + val aliases: List, + val doc: String?, + val dec: Descriptors.EnumDescriptor, +) { + data class Entry( + val name: FqName, + val doc: String?, + val dec: Descriptors.EnumValueDescriptor, + ) + + data class Alias( + val name: FqName, + val original: Entry, + val doc: String?, + val dec: Descriptors.EnumValueDescriptor, + ) +} + +data class OneOfDeclaration( + val name: FqName, + val variants: List, + val dec: Descriptors.OneofDescriptor +) + +data class FieldDeclaration( + val name: String, + val type: FieldType, + val doc: String?, + val dec: Descriptors.FieldDescriptor +) { + val packedFixedSize = type.wireType == WireType.FIXED64 || type.wireType == WireType.FIXED32 + + // aligns with edition settings and backward compatibility with proto2 and proto3 + val nullable: Boolean = dec.hasPresence() && !dec.isRequired && !dec.hasDefaultValue() + val number: Int = dec.number +} + +data class ServiceDeclaration( + val name: FqName, + val methods: List, + val dec: Descriptors.ServiceDescriptor, +) + +data class MethodDeclaration( + val name: String, + val inputType: MessageDeclaration, + val outputType: MessageDeclaration, + val dec: Descriptors.MethodDescriptor, +) +