diff --git a/build.gradle.kts b/build.gradle.kts index 2570a4f9b..0a529071c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,15 +3,16 @@ */ import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion -import util.configureApiValidation -import util.configureNpm -import util.configureProjectReport import util.libs +import util.configureProjectReport +import util.configureNpm +import util.configureApiValidation plugins { alias(libs.plugins.serialization) apply false alias(libs.plugins.kotlinx.rpc) apply false alias(libs.plugins.conventions.kover) + id("com.google.protobuf") version "0.9.4" apply false alias(libs.plugins.conventions.gradle.doctor) alias(libs.plugins.atomicfu) id("build-util") diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt index fa5c429b0..96221d0aa 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt @@ -105,6 +105,14 @@ internal class RpcIrContext( getRpcIrClassSymbol("RpcServiceDescriptor", "descriptor") } + val grpcServiceDescriptor by lazy { + getIrClassSymbol("kotlinx.rpc.grpc.descriptor", "GrpcServiceDescriptor") + } + + val grpcDelegate by lazy { + getIrClassSymbol("kotlinx.rpc.grpc.descriptor", "GrpcDelegate") + } + val rpcType by lazy { getRpcIrClassSymbol("RpcType", "descriptor") } @@ -262,6 +270,10 @@ internal class RpcIrContext( rpcServiceDescriptor.namedProperty("fqName") } + val grpcServiceDescriptorDelegate by lazy { + grpcServiceDescriptor.namedProperty("delegate") + } + private fun IrClassSymbol.namedProperty(name: String): IrPropertySymbol { return owner.properties.single { it.name.asString() == name }.symbol } @@ -276,7 +288,7 @@ internal class RpcIrContext( return getIrClassSymbol("kotlinx.rpc$suffix", name) } - private fun getIrClassSymbol(packageName: String, name: String): IrClassSymbol { + fun getIrClassSymbol(packageName: String, name: String): IrClassSymbol { return versionSpecificApi.referenceClass(pluginContext, packageName, name) ?: error("Unable to find symbol. Package: $packageName, name: $name") } diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt index b2d8dd78a..16502f602 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrServiceProcessor.kt @@ -16,7 +16,9 @@ internal class RpcIrServiceProcessor( private val logger: MessageCollector, ) : IrElementTransformer { override fun visitClass(declaration: IrClass, data: RpcIrContext): IrStatement { - if (declaration.hasAnnotation(RpcClassId.rpcAnnotation)) { + if (declaration.hasAnnotation(RpcClassId.rpcAnnotation) + || declaration.hasAnnotation(RpcClassId.grpcAnnotation) + ) { processService(declaration, data) } diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt index 158788c12..0251ab2bc 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt @@ -44,6 +44,10 @@ private object Descriptor { const val CREATE_INSTANCE = "createInstance" } +private object GrpcDescriptor { + const val DELEGATE = "delegate" +} + @Suppress("detekt.LargeClass", "detekt.TooManyFunctions") internal class RpcStubGenerator( private val declaration: ServiceDeclaration, @@ -123,7 +127,10 @@ internal class RpcStubGenerator( clientProperty() - coroutineContextProperty() + // not for gRPC + if (!declaration.isGrpc) { + coroutineContextProperty() + } declaration.fields.forEach { rpcFlowField(it) @@ -550,7 +557,15 @@ internal class RpcStubGenerator( overriddenSymbols = listOf(method.function.symbol) body = irBuilder(symbol).irBlockBody { - +irReturn( + val call = if (declaration.isGrpc) { + irRpcMethodClientCall( + method = method, + functionThisReceiver = functionThisReceiver, + isMethodObject = isMethodObject, + methodClass = methodClass, + arguments = arguments, + ) + } else { irCall( callee = ctx.functions.scopedClientCall, type = method.function.returnType, @@ -600,7 +615,9 @@ internal class RpcStubGenerator( putValueArgument(1, lambda) } - ) + } + + +irReturn(call) } } } @@ -868,7 +885,10 @@ internal class RpcStubGenerator( stubCompanionObjectThisReceiver = thisReceiver ?: error("Stub companion object expected to have thisReceiver: ${name.asString()}") - superTypes = listOf(ctx.rpcServiceDescriptor.typeWith(declaration.serviceType)) + superTypes = listOfNotNull( + ctx.rpcServiceDescriptor.typeWith(declaration.serviceType), + if (declaration.isGrpc) ctx.grpcServiceDescriptor.typeWith(declaration.serviceType) else null, + ) generateCompanionObjectConstructor() @@ -901,6 +921,10 @@ internal class RpcStubGenerator( generateCreateInstanceFunction() generateGetFieldsFunction() + + if (declaration.isGrpc) { + generateGrpcDelegateProperty() + } } /** @@ -1488,6 +1512,43 @@ internal class RpcStubGenerator( } } + /** + * override val delegate: GrpcDelegate = MyServiceDelegate + */ + private fun IrClass.generateGrpcDelegateProperty() { + addProperty { + name = Name.identifier(GrpcDescriptor.DELEGATE) + visibility = DescriptorVisibilities.PUBLIC + }.apply { + overriddenSymbols = listOf(ctx.properties.grpcServiceDescriptorDelegate) + + addBackingFieldUtil { + visibility = DescriptorVisibilities.PRIVATE + type = ctx.grpcDelegate.defaultType + vsApi { isFinalVS = true } + }.apply { + initializer = factory.createExpressionBody( + IrGetObjectValueImpl( + startOffset = UNDEFINED_OFFSET, + endOffset = UNDEFINED_OFFSET, + type = ctx.grpcDelegate.defaultType, + symbol = ctx.getIrClassSymbol( + declaration.service.packageFqName?.asString() + ?: error("Expected package name fro service ${declaration.service.name}"), + "${declaration.service.name.asString()}Delegate", + ), + ) + ) + } + + addDefaultGetter(this@generateGrpcDelegateProperty, ctx.irBuiltIns) { + visibility = DescriptorVisibilities.PUBLIC + overriddenSymbols = listOf(ctx.properties.grpcServiceDescriptorDelegate.owner.getterOrFail.symbol) + } + } + } + + // Associated object annotation works on JS, WASM, and Native platforms. // See https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/find-associated-object.html private fun addAssociatedObjectAnnotationIfPossible() { diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt index 3c3dce74a..9bcb31db5 100644 --- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt +++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt @@ -4,12 +4,14 @@ package kotlinx.rpc.codegen.extension +import kotlinx.rpc.codegen.common.RpcClassId import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrProperty import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.util.defaultType +import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.util.kotlinFqName class ServiceDeclaration( @@ -18,6 +20,7 @@ class ServiceDeclaration( val methods: List, val fields: List, ) { + val isGrpc = service.hasAnnotation(RpcClassId.grpcAnnotation) val fqName = service.kotlinFqName.asString() val serviceType = service.defaultType diff --git a/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt index c5da2bcaa..630b0d5f9 100644 --- a/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt +++ b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt @@ -12,6 +12,7 @@ import org.jetbrains.kotlin.name.Name object RpcClassId { val remoteServiceInterface = ClassId(FqName("kotlinx.rpc"), Name.identifier("RemoteService")) val rpcAnnotation = ClassId(FqName("kotlinx.rpc.annotations"), Name.identifier("Rpc")) + val grpcAnnotation = ClassId(FqName("kotlinx.rpc.grpc.annotations"), Name.identifier("Grpc")) val checkedTypeAnnotation = ClassId(FqName("kotlinx.rpc.annotations"), Name.identifier("CheckedTypeAnnotation")) val serializableAnnotation = ClassId(FqName("kotlinx.serialization"), Name.identifier("Serializable")) diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt index 6dd609833..33494a981 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirGenerationKeys.kt @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlinx.serialization.compiler.fir.SerializationPluginKey internal class RpcGeneratedStubKey( + val isGrpc: Boolean, private val serviceName: Name, val functions: List>, ) : GeneratedDeclarationKey() { @@ -25,6 +26,7 @@ internal val FirBasedSymbol<*>.generatedRpcServiceStubKey: RpcGeneratedStubKey? (origin as? FirDeclarationOrigin.Plugin)?.key as? RpcGeneratedStubKey internal class RpcGeneratedRpcMethodClassKey( + val isGrpc: Boolean, val rpcMethod: FirFunctionSymbol<*>, ) : GeneratedDeclarationKey() { val isObject = rpcMethod.valueParameterSymbols.isEmpty() diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt index 2a8f38125..368614431 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt @@ -25,6 +25,7 @@ class FirRpcAdditionalCheckers( ) : FirAdditionalCheckersExtension(session) { override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(FirRpcPredicates.rpc) + register(FirRpcPredicates.grpc) register(FirRpcPredicates.checkedAnnotationMeta) } diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt index 0ae4f2974..95183ad6f 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcPredicates.kt @@ -12,6 +12,10 @@ object FirRpcPredicates { annotated(RpcClassId.rpcAnnotation.asSingleFqName()) // @Rpc } + internal val grpc = DeclarationPredicate.create { + annotated(RpcClassId.grpcAnnotation.asSingleFqName()) // @Grpc + } + internal val checkedAnnotationMeta = DeclarationPredicate.create { metaAnnotated(RpcClassId.checkedTypeAnnotation.asSingleFqName(), includeItself = false) } diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcServiceGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcServiceGenerator.kt index 3dcdd998f..1062a0893 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcServiceGenerator.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcServiceGenerator.kt @@ -75,6 +75,7 @@ class FirRpcServiceGenerator( override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(FirRpcPredicates.rpc) + register(FirRpcPredicates.grpc) } /** @@ -112,7 +113,7 @@ class FirRpcServiceGenerator( val rpcMethodClassKey = classSymbol.generatedRpcMethodClassKey return when { - rpcMethodClassKey != null -> { + rpcMethodClassKey != null && !rpcMethodClassKey.isGrpc -> { when { !rpcMethodClassKey.isObject -> setOf( SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT, @@ -130,7 +131,10 @@ class FirRpcServiceGenerator( SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT } - classSymbol.isInterface && session.predicateBasedProvider.matches(FirRpcPredicates.rpc, classSymbol) -> { + classSymbol.isInterface && ( + session.predicateBasedProvider.matches(FirRpcPredicates.rpc, classSymbol) || + session.predicateBasedProvider.matches(FirRpcPredicates.grpc, classSymbol) + ) -> { setOf(RpcNames.SERVICE_STUB_NAME) } @@ -159,7 +163,7 @@ class FirRpcServiceGenerator( generateRpcMethodClass(owner, name, rpcServiceStubKey) } - owner.generatedRpcMethodClassKey != null -> { + owner.generatedRpcMethodClassKey != null && owner.generatedRpcMethodClassKey?.isGrpc == false -> { generateNestedClassLikeDeclarationWithSerialization(owner, name) } @@ -191,7 +195,7 @@ class FirRpcServiceGenerator( val methodName = name.rpcMethodName val rpcMethod = rpcServiceStubKey.functions.singleOrNull { it.name == methodName } ?: return null - val rpcMethodClassKey = RpcGeneratedRpcMethodClassKey(rpcMethod) + val rpcMethodClassKey = RpcGeneratedRpcMethodClassKey(rpcServiceStubKey.isGrpc, rpcMethod) val classKind = if (rpcMethodClassKey.isObject) ClassKind.OBJECT else ClassKind.CLASS val rpcMethodClass = createNestedClass( @@ -204,13 +208,15 @@ class FirRpcServiceGenerator( modality = Modality.FINAL } - rpcMethodClass.addAnnotation(RpcClassId.serializableAnnotation, session) + if (!session.predicateBasedProvider.matches(FirRpcPredicates.grpc, owner)) { + rpcMethodClass.addAnnotation(RpcClassId.serializableAnnotation, session) + } /** * Required to pass isSerializableObjectAndNeedsFactory check * from [SerializationFirSupertypesExtension]. */ - if (!isJvmOrMetadata && rpcMethodClassKey.isObject) { + if (!isJvmOrMetadata && rpcMethodClassKey.isObject && !rpcMethodClassKey.isGrpc) { rpcMethodClass.replaceSuperTypeRefs(createSerializationFactorySupertype()) } @@ -265,7 +271,13 @@ class FirRpcServiceGenerator( .filterIsInstance() .map { it.symbol } - return createNestedClass(owner, RpcNames.SERVICE_STUB_NAME, RpcGeneratedStubKey(owner.name, functions)) { + val key = RpcGeneratedStubKey( + isGrpc = session.predicateBasedProvider.matches(FirRpcPredicates.grpc, owner), + serviceName = owner.name, + functions = functions, + ) + + return createNestedClass(owner, RpcNames.SERVICE_STUB_NAME, key) { visibility = Visibilities.Public modality = Modality.FINAL }.symbol @@ -299,7 +311,7 @@ class FirRpcServiceGenerator( context: MemberGenerationContext, rpcMethodClassKey: RpcGeneratedRpcMethodClassKey, ): Set { - return if (rpcMethodClassKey.isObject) { + return if (rpcMethodClassKey.isObject && !rpcMethodClassKey.isGrpc) { // add .serializer() method for a serializable object serializationExtension.getCallableNamesForClass(classSymbol, context) } else { diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt index b6f17d375..0b9acf49a 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt @@ -20,6 +20,7 @@ abstract class FirRpcSupertypeGeneratorAbstract( ) : FirSupertypeGenerationExtension(session) { override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(FirRpcPredicates.rpc) + register(FirRpcPredicates.grpc) } override fun needTransformSupertypes(declaration: FirClassLikeDeclaration): Boolean { diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt index a7e053662..3232860cf 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt @@ -26,8 +26,9 @@ class FirRpcAnnotationChecker(private val ctx: FirCheckersContext) : FirRegularC reporter: DiagnosticReporter, ) { val rpcAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.rpc, declaration) + val grpcAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.grpc, declaration) - if (!declaration.isInterface && rpcAnnotated) { + if (!declaration.isInterface && (rpcAnnotated || grpcAnnotated)) { reporter.reportOn( source = declaration.symbol.rpcAnnotationSource(context.session), factory = FirRpcDiagnostics.WRONG_RPC_ANNOTATION_TARGET, @@ -43,7 +44,7 @@ class FirRpcAnnotationChecker(private val ctx: FirCheckersContext) : FirRegularC ) } - if (rpcAnnotated && !ctx.serializationIsPresent) { + if ((rpcAnnotated || grpcAnnotated) && !ctx.serializationIsPresent) { reporter.reportOn( source = declaration.symbol.rpcAnnotationSource(context.session), factory = FirRpcDiagnostics.MISSING_SERIALIZATION_MODULE, diff --git a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt index 145f60899..6dd3b8080 100644 --- a/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt +++ b/core/src/jvmMain/kotlin/kotlinx/rpc/internal/internalServiceDescriptorOf.jvm.kt @@ -13,8 +13,12 @@ private const val RPC_SERVICE_STUB_SIMPLE_NAME = "\$rpcServiceStub" internal actual fun <@Rpc T : Any> internalServiceDescriptorOf(kClass: KClass): Any? { val className = "${kClass.qualifiedName}\$$RPC_SERVICE_STUB_SIMPLE_NAME" - return kClass.java.classLoader - .loadClass(className) - ?.kotlin - ?.companionObjectInstance + return try { + kClass.java.classLoader + .loadClass(className) + ?.kotlin + ?.companionObjectInstance + } catch (_ : ClassNotFoundException) { + null + } } diff --git a/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt b/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt index bf277e849..46c1ab7c8 100644 --- a/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt +++ b/gradle-conventions/common/src/main/kotlin/util/apiValidation.kt @@ -14,12 +14,14 @@ fun Project.configureApiValidation() { the().apply { ignoredPackages.add("kotlinx.rpc.internal") ignoredPackages.add("kotlinx.rpc.krpc.internal") + ignoredPackages.add("kotlinx.rpc.grpc.internal") ignoredProjects.addAll( listOf( "compiler-plugin-tests", "krpc-test", "utils", + "protobuf-plugin", ) ) diff --git a/grpc/grpc-core/build.gradle.kts b/grpc/grpc-core/build.gradle.kts new file mode 100644 index 000000000..aae3bbd8a --- /dev/null +++ b/grpc/grpc-core/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + alias(libs.plugins.conventions.kmp) + alias(libs.plugins.kotlinx.rpc) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(projects.core) + api(libs.coroutines.core) + } + } + + jvmMain { + dependencies { + implementation("io.grpc:grpc-util:1.68.2") + implementation("io.grpc:grpc-stub:1.68.2") + implementation("io.grpc:grpc-protobuf:1.68.2") + implementation("io.grpc:grpc-kotlin-stub:1.4.1") + implementation("com.google.protobuf:protobuf-java-util:4.28.2") + implementation("com.google.protobuf:protobuf-kotlin:4.28.2") + } + } + } +} diff --git a/grpc/grpc-core/gradle.properties b/grpc/grpc-core/gradle.properties new file mode 100644 index 000000000..8be80e5ef --- /dev/null +++ b/grpc/grpc-core/gradle.properties @@ -0,0 +1,8 @@ +# +# Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +kotlinx.rpc.excludeWasmWasi=true +kotlinx.rpc.excludeWasmJs=true +kotlinx.rpc.excludeJs=true +kotlinx.rpc.excludeNative=true diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt new file mode 100644 index 000000000..cb36ba3b5 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcClient.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.job +import kotlinx.rpc.RpcCall +import kotlinx.rpc.RpcClient +import kotlinx.rpc.grpc.descriptor.GrpcClientDelegate +import kotlinx.rpc.grpc.descriptor.GrpcServiceDescriptor +import kotlinx.rpc.internal.utils.map.ConcurrentHashMap +import kotlin.coroutines.CoroutineContext + +public class GrpcClient(private val channel: ManagedChannel) : RpcClient { + override val coroutineContext: CoroutineContext = SupervisorJob() + + private val stubs = ConcurrentHashMap() + + override suspend fun call(call: RpcCall): T { + return call.delegate().call(call) + } + + override fun callAsync(serviceScope: CoroutineScope, call: RpcCall): Deferred { + return call.delegate().callAsync(call) + } + + private fun RpcCall.delegate(): GrpcClientDelegate { + val grpc = (descriptor as? GrpcServiceDescriptor<*>) + ?: error("Service ${descriptor.fqName} is not a gRPC service") + + return stubs.computeIfAbsent(serviceId) { grpc.delegate.clientProvider(channel) } + } + + override fun provideStubContext(serviceId: Long): CoroutineContext { + // todo create lifetime hierarchy if possible + return SupervisorJob(coroutineContext.job) + } +} + +public fun GrpcClient( + name: String, + port: Int, + configure: ManagedChannelBuilder<*>.() -> Unit = {}, +): GrpcClient { + val channel = ManagedChannelBuilder(name, port).apply(configure).buildChannel() + return GrpcClient(channel) +} + +public fun GrpcClient( + target: String, + configure: ManagedChannelBuilder<*>.() -> Unit = {}, +): GrpcClient { + val channel = ManagedChannelBuilder(target).apply(configure).buildChannel() + return GrpcClient(channel) +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt new file mode 100644 index 000000000..539d9c8e9 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.job +import kotlinx.rpc.RpcServer +import kotlinx.rpc.descriptor.serviceDescriptorOf +import kotlinx.rpc.grpc.annotations.Grpc +import kotlinx.rpc.grpc.descriptor.GrpcServiceDescriptor +import kotlinx.rpc.grpc.internal.MutableHandlerRegistry +import kotlinx.rpc.grpc.internal.ServerServiceDefinition +import kotlin.coroutines.CoroutineContext +import kotlin.reflect.KClass +import kotlin.time.Duration + +public class GrpcServer internal constructor( + override val port: Int = 8080, + builder: ServerBuilder<*>.() -> Unit, +) : RpcServer, Server { + private var isBuilt = false + private lateinit var internalServer: Server + + private val serverBuilder: ServerBuilder<*> = ServerBuilder(port).apply(builder) + private val registry: MutableHandlerRegistry by lazy { + MutableHandlerRegistry().apply { serverBuilder.fallbackHandlerRegistry(this) } + } + + override val coroutineContext: CoroutineContext + get() = error("coroutineContext is not available for gRPC server builder") + + override fun <@Grpc Service : Any> registerService( + serviceKClass: KClass, + serviceFactory: (CoroutineContext) -> Service, + ) { + val childJob = SupervisorJob() + val service = serviceFactory(childJob) + + val definition: ServerServiceDefinition = getDefinition(service, serviceKClass) + + if (isBuilt) { + registry.addService(definition) + } else { + serverBuilder.addService(definition) + } + } + + private fun <@Grpc Service : Any> getDefinition( + service: Service, + serviceKClass: KClass, + ): ServerServiceDefinition { + val descriptor = serviceDescriptorOf(serviceKClass) + val grpc = (descriptor as? GrpcServiceDescriptor) + ?: error("Service ${descriptor.fqName} is not a gRPC service") + + return grpc.delegate.definitionFor(service) + } + + internal fun build() { + internalServer = Server(serverBuilder) + isBuilt = true + } + + override val isShutdown: Boolean + get() = internalServer.isShutdown + + override val isTerminated: Boolean + get() = internalServer.isTerminated + + override fun start(): GrpcServer { + internalServer.start() + return this + } + + override fun shutdown(): GrpcServer { + internalServer.shutdown() + return this + } + + override fun shutdownNow(): GrpcServer { + internalServer.shutdownNow() + return this + } + + override suspend fun awaitTermination(duration: Duration): GrpcServer { + internalServer.awaitTermination(duration) + return this + } +} + +public fun GrpcServer( + port: Int, + configure: ServerBuilder<*>.() -> Unit = {}, + builder: RpcServer.() -> Unit = {}, +): GrpcServer { + return GrpcServer(port, configure).apply(builder).apply { build() } +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt new file mode 100644 index 000000000..a8eb83bfc --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +import kotlin.time.Duration + +public expect abstract class ManagedChannelPlatform + +public interface ManagedChannel { + public val isShutdown: Boolean + public val isTerminated: Boolean + + public suspend fun awaitTermination(duration: Duration): Boolean + + public fun shutdown(): ManagedChannel + public fun shutdownNow(): ManagedChannel + + public val platformApi: ManagedChannelPlatform +} + +public expect abstract class ManagedChannelBuilder> + +public expect fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> +public expect fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> + +public expect fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt new file mode 100644 index 000000000..bb80d3040 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +import kotlinx.rpc.grpc.internal.HandlerRegistry +import kotlinx.rpc.grpc.internal.ServerServiceDefinition +import kotlin.time.Duration + +public expect abstract class ServerBuilder> { + public abstract fun addService(service: ServerServiceDefinition): T + + public abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal expect fun ServerBuilder(port: Int): ServerBuilder<*> + +public interface Server { + public val port: Int + public val isShutdown: Boolean + public val isTerminated: Boolean + + public fun start() : Server + public fun shutdown() : Server + public fun shutdownNow() : Server + public suspend fun awaitTermination(duration: Duration = Duration.INFINITE) : Server +} + +internal expect fun Server(builder: ServerBuilder<*>): Server diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt new file mode 100644 index 000000000..a15b6f747 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("MemberVisibilityCanBePrivate") + +package kotlinx.rpc.grpc + +public interface Status { + public val code: Code + public val description: String? + public val cause: Throwable? + + public enum class Code(public val value: Int) { + OK(0), + CANCELLED(1), + UNKNOWN(2), + INVALID_ARGUMENT(3), + DEADLINE_EXCEEDED(4), + NOT_FOUND(5), + ALREADY_EXISTS(6), + PERMISSION_DENIED(7), + RESOURCE_EXHAUSTED(8), + FAILED_PRECONDITION(9), + ABORTED(10), + OUT_OF_RANGE(11), + UNIMPLEMENTED(12), + INTERNAL(13), + UNAVAILABLE(14), + DATA_LOSS(15), + UNAUTHENTICATED(16); + + public val valueAscii: ByteArray = value.toString().toByteArray(Charsets.US_ASCII) + } +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.kt new file mode 100644 index 000000000..447befcae --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public interface StatusRuntimeException { + public val status: Status +} + +public expect fun StatusRuntimeException(status: Status) : StatusRuntimeException diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/annotations/Grpc.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/annotations/Grpc.kt new file mode 100644 index 000000000..e8bfef39d --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/annotations/Grpc.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.annotations + +import kotlinx.rpc.annotations.Rpc + +@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.TYPE_PARAMETER) +@Rpc +public annotation class Grpc diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor.kt new file mode 100644 index 000000000..522174082 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.descriptor + +import kotlinx.coroutines.Deferred +import kotlinx.rpc.RpcCall +import kotlinx.rpc.descriptor.RpcServiceDescriptor +import kotlinx.rpc.grpc.ManagedChannel +import kotlinx.rpc.grpc.annotations.Grpc +import kotlinx.rpc.grpc.internal.ServerServiceDefinition +import kotlinx.rpc.internal.utils.ExperimentalRpcApi + +@ExperimentalRpcApi +public interface GrpcServiceDescriptor<@Grpc T : Any> : RpcServiceDescriptor { + public val delegate: GrpcDelegate +} + +@ExperimentalRpcApi +public interface GrpcDelegate<@Grpc T : Any> { + public fun clientProvider(channel: ManagedChannel): GrpcClientDelegate + + public fun definitionFor(impl: T): ServerServiceDefinition +} + +@ExperimentalRpcApi +public interface GrpcClientDelegate { + public suspend fun call(rpcCall: RpcCall): R + + public fun callAsync(rpcCall: RpcCall): Deferred +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/MutableHandlerRegistry.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/MutableHandlerRegistry.kt new file mode 100644 index 000000000..55b89d93e --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/MutableHandlerRegistry.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc.internal + +import kotlinx.rpc.internal.utils.InternalRpcApi + +@InternalRpcApi +public expect abstract class HandlerRegistry + +@InternalRpcApi +public expect class MutableHandlerRegistry constructor() : HandlerRegistry { + internal fun addService(service: ServerServiceDefinition): ServerServiceDefinition? +} diff --git a/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/ServerServiceDefinition.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/ServerServiceDefinition.kt new file mode 100644 index 000000000..535be218b --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/internal/ServerServiceDefinition.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc.internal + +import kotlinx.rpc.internal.utils.InternalRpcApi + +@InternalRpcApi +public expect class ServerServiceDefinition diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt new file mode 100644 index 000000000..edbb7c065 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.jvm.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.time.Duration + +public actual typealias ManagedChannelPlatform = io.grpc.ManagedChannel + +public actual typealias ManagedChannelBuilder = io.grpc.ManagedChannelBuilder + +public actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + return build().toKotlin() +} + +public actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + return io.grpc.ManagedChannelBuilder.forAddress(name, port) +} + +public actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + return io.grpc.ManagedChannelBuilder.forTarget(target) +} + +public fun io.grpc.ManagedChannel.toKotlin(): ManagedChannel { + return JvmManagedChannel(this) +} + +private class JvmManagedChannel(private val channel: io.grpc.ManagedChannel) : ManagedChannel { + override val isShutdown: Boolean + get() = channel.isShutdown + + override val isTerminated: Boolean + get() = channel.isTerminated + + override suspend fun awaitTermination(duration: Duration): Boolean { + return withContext(Dispatchers.IO) { + channel.awaitTermination(duration.inWholeNanoseconds, java.util.concurrent.TimeUnit.NANOSECONDS) + } + } + + override fun shutdown(): ManagedChannel { + channel.shutdown() + return this + } + + override fun shutdownNow(): ManagedChannel { + channel.shutdownNow() + return this + } + + override val platformApi: ManagedChannelPlatform + get() = channel +} diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt new file mode 100644 index 000000000..b4a6c8d55 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Server.jvm.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +public actual typealias ServerBuilder = io.grpc.ServerBuilder + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + return io.grpc.ServerBuilder.forPort(port) +} + + +internal actual fun Server(builder: ServerBuilder<*>): Server { + return builder.build().toKotlin() +} + +private fun io.grpc.Server.toKotlin(): Server { + return object : Server { + override val port: Int + get() = this@toKotlin.port + + override val isShutdown: Boolean + get() = this@toKotlin.isShutdown + + override val isTerminated: Boolean + get() = this@toKotlin.isTerminated + + override fun start() : Server { + this@toKotlin.start() + return this + } + + override fun shutdown(): Server { + this@toKotlin.shutdown() + return this + } + + override fun shutdownNow(): Server { + this@toKotlin.shutdownNow() + return this + } + + override suspend fun awaitTermination(duration: Duration): Server { + withContext(Dispatchers.IO) { + if (duration == Duration.INFINITE) { + this@toKotlin.awaitTermination() + } else { + this@toKotlin.awaitTermination(duration.inWholeNanoseconds, TimeUnit.NANOSECONDS) + } + } + return this + } + } +} diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Status.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Status.jvm.kt new file mode 100644 index 000000000..bd4aaa48a --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/Status.jvm.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("detekt.CyclomaticComplexMethod") + +package kotlinx.rpc.grpc + +internal fun Status.toJvm(): io.grpc.Status { + val code = when (code) { + Status.Code.OK -> io.grpc.Status.Code.OK + Status.Code.CANCELLED -> io.grpc.Status.Code.CANCELLED + Status.Code.UNKNOWN -> io.grpc.Status.Code.UNKNOWN + Status.Code.INVALID_ARGUMENT -> io.grpc.Status.Code.INVALID_ARGUMENT + Status.Code.DEADLINE_EXCEEDED -> io.grpc.Status.Code.DEADLINE_EXCEEDED + Status.Code.NOT_FOUND -> io.grpc.Status.Code.NOT_FOUND + Status.Code.ALREADY_EXISTS -> io.grpc.Status.Code.ALREADY_EXISTS + Status.Code.PERMISSION_DENIED -> io.grpc.Status.Code.PERMISSION_DENIED + Status.Code.RESOURCE_EXHAUSTED -> io.grpc.Status.Code.RESOURCE_EXHAUSTED + Status.Code.FAILED_PRECONDITION -> io.grpc.Status.Code.FAILED_PRECONDITION + Status.Code.ABORTED -> io.grpc.Status.Code.ABORTED + Status.Code.OUT_OF_RANGE -> io.grpc.Status.Code.OUT_OF_RANGE + Status.Code.UNIMPLEMENTED -> io.grpc.Status.Code.UNIMPLEMENTED + Status.Code.INTERNAL -> io.grpc.Status.Code.INTERNAL + Status.Code.UNAVAILABLE -> io.grpc.Status.Code.UNAVAILABLE + Status.Code.DATA_LOSS -> io.grpc.Status.Code.DATA_LOSS + Status.Code.UNAUTHENTICATED -> io.grpc.Status.Code.UNAUTHENTICATED + } + + return io.grpc.Status.fromCode(code) + .withDescription(description) + .withCause(cause) +} + +@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") +internal fun io.grpc.Status.toKotlin(): Status { + val code = when (code) { + io.grpc.Status.Code.OK -> Status.Code.OK + io.grpc.Status.Code.CANCELLED -> Status.Code.CANCELLED + io.grpc.Status.Code.UNKNOWN -> Status.Code.UNKNOWN + io.grpc.Status.Code.INVALID_ARGUMENT -> Status.Code.INVALID_ARGUMENT + io.grpc.Status.Code.DEADLINE_EXCEEDED -> Status.Code.DEADLINE_EXCEEDED + io.grpc.Status.Code.NOT_FOUND -> Status.Code.NOT_FOUND + io.grpc.Status.Code.ALREADY_EXISTS -> Status.Code.ALREADY_EXISTS + io.grpc.Status.Code.PERMISSION_DENIED -> Status.Code.PERMISSION_DENIED + io.grpc.Status.Code.RESOURCE_EXHAUSTED -> Status.Code.RESOURCE_EXHAUSTED + io.grpc.Status.Code.FAILED_PRECONDITION -> Status.Code.FAILED_PRECONDITION + io.grpc.Status.Code.ABORTED -> Status.Code.ABORTED + io.grpc.Status.Code.OUT_OF_RANGE -> Status.Code.OUT_OF_RANGE + io.grpc.Status.Code.UNIMPLEMENTED -> Status.Code.UNIMPLEMENTED + io.grpc.Status.Code.INTERNAL -> Status.Code.INTERNAL + io.grpc.Status.Code.UNAVAILABLE -> Status.Code.UNAVAILABLE + io.grpc.Status.Code.DATA_LOSS -> Status.Code.DATA_LOSS + io.grpc.Status.Code.UNAUTHENTICATED -> Status.Code.UNAUTHENTICATED + } + + return JvmStatus(code, description, cause) +} + +internal class JvmStatus( + override val code: Status.Code, + override val description: String? = null, + override val cause: Throwable? = null, +): Status diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.jvm.kt new file mode 100644 index 000000000..b8fb2a13e --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.jvm.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + return io.grpc.StatusRuntimeException(status.toJvm()).toKotlin() +} + +internal class JvmStatusRuntimeException(override val status: Status) : StatusRuntimeException + +public fun io.grpc.StatusRuntimeException.toKotlin(): StatusRuntimeException { + return JvmStatusRuntimeException(status.toKotlin()) +} + +public fun StatusRuntimeException.toJvm(): io.grpc.StatusRuntimeException { + return io.grpc.StatusRuntimeException(status.toJvm()) +} diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/internal/MutableHandlerRegistry.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/internal/MutableHandlerRegistry.jvm.kt new file mode 100644 index 000000000..4ea687331 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/internal/MutableHandlerRegistry.jvm.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc.internal + +internal actual typealias HandlerRegistry = io.grpc.HandlerRegistry + +internal actual typealias MutableHandlerRegistry = io.grpc.util.MutableHandlerRegistry diff --git a/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/internal/ServerServiceDefinition.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/internal/ServerServiceDefinition.jvm.kt new file mode 100644 index 000000000..9f5efc063 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/internal/ServerServiceDefinition.jvm.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package kotlinx.rpc.grpc.internal + +import kotlinx.rpc.internal.utils.InternalRpcApi + +@InternalRpcApi +public actual typealias ServerServiceDefinition = io.grpc.ServerServiceDefinition diff --git a/protobuf-plugin/build.gradle.kts b/protobuf-plugin/build.gradle.kts new file mode 100644 index 000000000..262c5a0e2 --- /dev/null +++ b/protobuf-plugin/build.gradle.kts @@ -0,0 +1,109 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + +plugins { + alias(libs.plugins.conventions.jvm) + alias(libs.plugins.kotlinx.rpc) + alias(libs.plugins.serialization) + id("com.google.protobuf") +} + +dependencies { + implementation("com.google.protobuf:protobuf-java:3.24.1") + + implementation(libs.slf4j.api) + implementation(libs.logback.classic) + + testImplementation(projects.grpc.grpcCore) + testImplementation(libs.coroutines.core) + testImplementation(libs.kotlin.test) + testImplementation("io.grpc:grpc-stub:1.68.2") + testImplementation("io.grpc:grpc-netty:1.68.2") + testImplementation("io.grpc:grpc-protobuf:1.68.2") + testImplementation("io.grpc:grpc-kotlin-stub:1.4.1") + testImplementation("com.google.protobuf:protobuf-java-util:4.28.2") + testImplementation("com.google.protobuf:protobuf-kotlin:4.28.2") +} + +sourceSets { + test { + proto { + exclude( + "**/empty_deprecated.proto", + "**/enum.proto", + "**/example.proto", + "**/funny_types.proto", + "**/map.proto", + "**/multiple_files.proto", + "**/nested.proto", + "**/one_of.proto", + "**/options.proto", + "**/with_comments.proto", + ) + } + } +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "kotlinx.rpc.protobuf.MainKt" + + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + // Protoc plugins are all fat jars basically (the ones built on jvm) + // be really careful of what you put in the classpath here + from( + configurations.runtimeClasspath.map { prop -> + prop.map { if (it.isDirectory()) it else zipTree(it) } + } + ) +} + +val buildDirPath: String = project.layout.buildDirectory.get().asFile.absolutePath + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.24.1" + } + + plugins { + create("kotlinx-rpc") { + path = "$buildDirPath/libs/protobuf-plugin-$version.jar" + } + + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.57.2" + } + + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.3.1:jdk8@jar" + } + } + + generateProtoTasks { + all().matching { it.isTest }.all { + plugins { + create("kotlinx-rpc") { + option("debugOutput=$buildDirPath/protobuf-plugin.log") + option("messageMode=interface") + } + create("grpc") + create("grpckt") + } + + dependsOn(tasks.jar) + } + } +} + +kotlin { + explicitApi = ExplicitApiMode.Disabled +} + +tasks.test { + useJUnitPlatform() +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt new file mode 100644 index 000000000..26b996976 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt @@ -0,0 +1,300 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +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, +) { + private var isEmpty: Boolean = true + private var result: String? = null + private var lastIsDeclaration: Boolean = false + + @Suppress("FunctionName") + private fun _append( + value: String? = null, + newLineBefore: Boolean = false, + newLineAfter: Boolean = false, + newLineIfAbsent: Boolean = false, + ) { + var addedNewLineBefore = false + + if (lastIsDeclaration) { + builder.appendLine() + lastIsDeclaration = false + addedNewLineBefore = true + } else if (newLineIfAbsent) { + val last = builder.lastOrNull() + if (last != null && last != '\n') { + builder.appendLine() + addedNewLineBefore = true + } + } + + if (!addedNewLineBefore && newLineBefore) { + builder.appendLine() + } + if (value != null) { + builder.append(value) + } + if (newLineAfter) { + builder.appendLine() + } + + isEmpty = false + } + + private fun append(value: String) { + _append(value) + } + + private fun addLine(value: String? = null) { + _append("$indent${value ?: ""}", newLineIfAbsent = true) + } + + fun newLine() { + _append(newLineBefore = true) + } + + private fun withNextIndent(block: CodeGenerator.() -> Unit) { + CodeGenerator(parameters, "$indent$ONE_INDENT", builder, logger).block() + } + + internal fun scope(prefix: String, suffix: String = "", block: (CodeGenerator.() -> Unit)? = null) { + addLine(prefix) + scopeWithSuffix(suffix, block) + } + + private fun scopeWithSuffix(suffix: String = "", block: (CodeGenerator.() -> Unit)? = null) { + if (block == null) { + newLine() + lastIsDeclaration = true + return + } + + val nested = CodeGenerator(parameters, "$indent$ONE_INDENT", logger = logger).apply(block) + + if (nested.isEmpty) { + newLine() + lastIsDeclaration = true + return + } + + append(" {") + newLine() + append(nested.build().trimEnd()) + addLine("}$suffix") + newLine() + lastIsDeclaration = true + } + + fun code(code: String) { + code.lines().forEach { addLine(it) } + } + + fun property( + name: String, + modifiers: String = "", + contextReceiver: String = "", + type: String = "", + delegate: Boolean = false, + value: String = "", + block: (CodeGenerator.() -> Unit)? = null, + ) { + val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers " + val contextString = if (contextReceiver.isEmpty()) "" else "$contextReceiver." + val typeString = if (type.isEmpty()) "" else ": $type" + val delegateString = if (delegate) " by " else " = " + scope("${modifiersString}val $contextString$name$typeString$delegateString$value", block = block) + } + + fun function( + name: String, + modifiers: String = "", + typeParameters: String = "", + args: String = "", + contextReceiver: String = "", + returnType: String = "", + block: (CodeGenerator.() -> Unit)? = null, + ) { + val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers " + val contextString = if (contextReceiver.isEmpty()) "" else "$contextReceiver." + val returnTypeString = if (returnType.isEmpty()) "" else ": $returnType" + val typeParametersString = if (typeParameters.isEmpty()) "" else " <$typeParameters>" + scope("${modifiersString}fun$typeParametersString $contextString$name($args)$returnTypeString", block = block) + } + + enum class DeclarationType(val strValue: String) { + Class("class"), Interface("interface"), Object("object"); + } + + @JvmName("clazz_no_constructorArgs") + fun clazz( + name: String, + modifiers: String = "", + superTypes: List = emptyList(), + annotations: List = emptyList(), + declarationType: DeclarationType = DeclarationType.Class, + block: (CodeGenerator.() -> Unit)? = null, + ) { + clazz( + name = name, + modifiers = modifiers, + constructorArgs = emptyList(), + superTypes = superTypes, + annotations = annotations, + declarationType = declarationType, + block = block, + ) + } + + @JvmName("clazz_constructorArgs_no_default") + fun clazz( + name: String, + modifiers: String = "", + constructorArgs: List = emptyList(), + superTypes: List = emptyList(), + annotations: List = emptyList(), + declarationType: DeclarationType = DeclarationType.Class, + block: (CodeGenerator.() -> Unit)? = null, + ) { + clazz( + name = name, + modifiers = modifiers, + constructorArgs = constructorArgs.map { it to null }, + superTypes = superTypes, + annotations = annotations, + declarationType = declarationType, + block = block, + ) + } + + fun clazz( + name: String, + modifiers: String = "", + constructorArgs: List> = emptyList(), + superTypes: List = emptyList(), + annotations: List = emptyList(), + declarationType: DeclarationType = DeclarationType.Class, + block: (CodeGenerator.() -> Unit)? = null, + ) { + for (annotation in annotations) { + addLine(annotation) + } + + val modifiersString = if (modifiers.isEmpty()) "" else "$modifiers " + + val firstLine = "$modifiersString${declarationType.strValue}${if (name.isNotEmpty()) " " else ""}$name" + addLine(firstLine) + + val shouldPutArgsOnNewLines = + firstLine.length + constructorArgs.sumOf { + it.first.length + (it.second?.length?.plus(3) ?: 0) + 2 + } + indent.length > 80 + + val constructorArgsTransformed = constructorArgs.map { (arg, default) -> + val defaultString = default?.let { " = $it" } ?: "" + "$arg$defaultString" + } + + when { + shouldPutArgsOnNewLines && constructorArgsTransformed.isNotEmpty() -> { + append("(") + newLine() + withNextIndent { + for (arg in constructorArgsTransformed) { + addLine("$arg,") + } + } + addLine(")") + } + + constructorArgsTransformed.isNotEmpty() -> { + append("(") + append(constructorArgsTransformed.joinToString(", ")) + append(")") + } + } + + val superString = superTypes + .takeIf { it.isNotEmpty() } + ?.joinToString(", ") + ?.let { ": $it" } + ?: "" + + append(superString) + + scopeWithSuffix(block = block) + } + + open fun build(): String { + if (result == null) { + result = builder.toString() + } + + return result!! + } + + companion object { + private const val ONE_INDENT = " " + } +} + +class FileGenerator( + codeGenerationParameters: CodeGenerationParameters, + var filename: String? = null, + var packageName: String? = null, + logger: Logger = NOPLogger.NOP_LOGGER, +) : CodeGenerator(codeGenerationParameters, "", logger = logger) { + private val imports = mutableListOf() + + fun importPackage(name: String) { + if (name != packageName) { + imports.add("$name.*") + } + } + + fun import(name: String) { + imports.add(name) + } + + override fun build(): String { + val sortedImports = imports.toSortedSet() + val prefix = buildString { + if (packageName != null) { + appendLine("package $packageName") + } + + appendLine() + + for (import in sortedImports) { + appendLine("import $import") + } + + if (imports.isNotEmpty()) { + appendLine() + } + } + + return prefix + super.build() + } +} + +fun file( + codeGenerationParameters: CodeGenerationParameters, + name: String? = null, + packageName: String? = null, + logger: Logger = NOPLogger.NOP_LOGGER, + block: FileGenerator.() -> Unit, +): FileGenerator = FileGenerator(codeGenerationParameters, name, packageName, logger).apply(block) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/Main.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/Main.kt new file mode 100644 index 000000000..e088ec8e6 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/Main.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023-2024 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.compiler.PluginProtos + +// todo +// type resolution (avoid over qualified types) +// comments +// extensions +// maps +// kmp sources sets +// platform specific bindings +// common API +// DSL builders +// kotlin_multiple_files, kotlin_package options +// library proto files +// explicit API mode +// services +// unfolded types overloads +// nested streams +// + +fun main() { + val inputBytes = System.`in`.readBytes() + val request = PluginProtos.CodeGeneratorRequest.parseFrom(inputBytes) + val plugin = RpcProtobufPlugin() + val output: PluginProtos.CodeGeneratorResponse = plugin.run(request) + output.writeTo(System.out) +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinGenerator.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinGenerator.kt new file mode 100644 index 000000000..0ee3a1120 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinGenerator.kt @@ -0,0 +1,361 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import kotlinx.rpc.protobuf.CodeGenerator.DeclarationType +import kotlinx.rpc.protobuf.model.* +import org.slf4j.Logger + +class ModelToKotlinGenerator( + private val model: Model, + private val logger: Logger, + private val codeGenerationParameters: CodeGenerationParameters, +) { + fun generateKotlinFiles(): List { + return model.files.map { it.generateKotlinFile() } + } + + private fun FileDeclaration.generateKotlinFile(): FileGenerator { + return file(codeGenerationParameters, logger = logger) { + filename = name.simpleName + packageName = name.packageName + + dependencies.forEach { dependency -> + importPackage(dependency.name.packageName) + } + + generateDeclaredEntities(this@generateKotlinFile) + + additionalImports.forEach { + import(it) + } + } + } + + private val additionalImports = mutableSetOf() + + private fun CodeGenerator.generateDeclaredEntities(fileDeclaration: FileDeclaration) { + fileDeclaration.messageDeclarations.forEach { generateMessage(it) } +// fileDeclaration.enumDeclarations.forEach { generateEnum(it) } + fileDeclaration.serviceDeclarations.forEach { generateService(it) } + } + + @Suppress("detekt.CyclomaticComplexMethod") + private fun CodeGenerator.generateMessage(declaration: MessageDeclaration) { + val fields = declaration.actualFields.map { it.generateFieldDeclaration() to it.type.defaultValue } + + val isInterfaceMode = parameters.messageMode == RpcProtobufPlugin.MessageMode.Interface + + val (declarationType, modifiers) = when { + isInterfaceMode -> { + DeclarationType.Interface to "" + } + + fields.isEmpty() -> { + DeclarationType.Object to "" + } + + else -> { + DeclarationType.Class to "data" + } + } + + clazz( + name = declaration.name.simpleName, + modifiers = modifiers, + constructorArgs = if (isInterfaceMode) emptyList() else fields.map { "val ${it.first}" to it.second }, + declarationType = declarationType, + ) { + if (isInterfaceMode) { + fields.forEach { + code("val ${it.first}") + newLine() + } + } + +// declaration.oneOfDeclarations.forEach { oneOf -> +// generateOneOf(oneOf) +// } +// +// declaration.nestedDeclarations.forEach { nested -> +// generateMessage(nested) +// } +// +// declaration.enumDeclarations.forEach { enum -> +// generateEnum(enum) +// } + + if (isInterfaceMode) { + clazz("", modifiers = "companion", declarationType = DeclarationType.Object) + } + } + + if (isInterfaceMode) { + clazz( + name = "${declaration.name.simpleName}Builder", + declarationType = DeclarationType.Class, + superTypes = listOf(declaration.name.simpleName), + ) { + fields.forEach { + code("override var ${it.first} = ${it.second}") + newLine() + } + } + + function( + name = "invoke", + modifiers = "operator", + args = "body: ${declaration.name.simpleName}Builder.() -> Unit", + contextReceiver = "${declaration.name.simpleName}.Companion", + returnType = declaration.name.simpleName, + ) { + code("return ${declaration.name.simpleName}Builder().apply(body)") + } + } + + val platformType = "${declaration.name.packageName}.${declaration.name.simpleName}OuterClass." + + declaration.name.simpleName + + function( + name = "toPlatform", + contextReceiver = declaration.name.simpleName, + returnType = platformType, + ) { + scope("return $platformType.newBuilder().apply", ".build()") { + declaration.actualFields.forEach { field -> + val call = "this@toPlatform.${field.name}${field.toPlatformCast()}" + code("set${field.name.replaceFirstChar { ch -> ch.uppercase() }}($call)") + } + } + } + + function( + name = "toKotlin", + contextReceiver = platformType, + returnType = declaration.name.simpleName, + ) { + scope("return ${declaration.name.simpleName}") { + declaration.actualFields.forEach { field -> + code("${field.name} = this@toKotlin.${field.name}${field.toKotlinCast()}") + } + } + } + } + + private fun FieldDeclaration.toPlatformCast(): String { + val type = type as? FieldType.IntegralType ?: return "" + + return when (type) { + 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) }" + else -> "" + } + } + + private fun FieldDeclaration.toKotlinCast(): String { + val type = type as? FieldType.IntegralType ?: return "" + + return when (type) { + FieldType.IntegralType.FIXED32 -> ".toUInt()" + FieldType.IntegralType.FIXED64 -> ".toULong()" + FieldType.IntegralType.UINT32 -> ".toUInt()" + FieldType.IntegralType.UINT64 -> ".toULong()" + FieldType.IntegralType.BYTES -> ".toByteArray()" + else -> "" + } + } + + private fun FieldDeclaration.generateFieldDeclaration(): String { + return "${name}: ${typeFqName()}" + } + + private fun FieldDeclaration.typeFqName(): String { + return when (type) { +// is FieldType.Reference -> { +// type.value.simpleName +// } + + is FieldType.IntegralType -> { + type.fqName.simpleName + } + +// is FieldType.List -> { +// "List<${type.valueName.simpleName}>" +// } +// +// is FieldType.Map -> { +// "Map<${type.keyName.simpleName}, ${type.valueName.simpleName}>" +// } + else -> { + error("Unsupported type: $type") + } + } + } + + @Suppress("unused") + private fun CodeGenerator.generateOneOf(declaration: OneOfDeclaration) { + val interfaceName = declaration.name.simpleName + + clazz(declaration.name.simpleName, "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), + ) + + additionalImports.add("kotlin.jvm.JvmInline") + } + } + } + + @Suppress("unused") + private fun CodeGenerator.generateEnum(declaration: EnumDeclaration) { + clazz(declaration.name.simpleName, "enum") { + code(declaration.originalEntries.joinToString(", ", postfix = ";") { enumEntry -> + enumEntry.name.simpleName + }) + + 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}" + ) + } + } + } + } + } + + @Suppress("detekt.LongMethod") + private fun CodeGenerator.generateService(service: ServiceDeclaration) { + code("@kotlinx.rpc.grpc.annotations.Grpc") + clazz(service.name.simpleName, declarationType = DeclarationType.Interface) { + service.methods.forEach { method -> + // no streaming for now + function( + name = method.name.simpleName, + modifiers = "suspend", + args = "message: ${method.inputType.simpleName}", + returnType = method.outputType.simpleName, + ) + } + } + + newLine() + + code("@Suppress(\"unused\", \"all\")") + clazz( + modifiers = "private", + name = "${service.name.simpleName}Delegate", + declarationType = DeclarationType.Object, + superTypes = listOf("kotlinx.rpc.grpc.descriptor.GrpcDelegate<${service.name.simpleName}>"), + ) { + 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.simpleName}", + returnType = "kotlinx.rpc.grpc.internal.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.simpleName}"), + ) { + service.methods.forEach { method -> + val grpcName = method.name.simpleName.replaceFirstChar { it.lowercase() } + + function( + name = grpcName, + modifiers = "override suspend", + args = "request: ${method.inputType.toPlatformMessageType()}", + returnType = method.outputType.toPlatformMessageType(), + ) { + code("return impl.${method.name.simpleName}(request.toKotlin()).toPlatform()") + } + } + } + + 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 = "call: kotlinx.rpc.RpcCall", + typeParameters = "R", + returnType = "R", + ) { + code("val message = (call.data as kotlinx.rpc.internal.RpcMethodClass).asArray()[0]") + code("@Suppress(\"UNCHECKED_CAST\")") + scope("return when (call.callableName)") { + service.methods.forEach { method -> + val grpcName = method.name.simpleName.replaceFirstChar { it.lowercase() } + val result = "stub.$grpcName((message as ${method.inputType.simpleName}).toPlatform())" + code("\"${method.name.simpleName}\" -> $result.toKotlin() as R") + } + + code("else -> error(\"Illegal call: \${call.callableName}\")") + } + } + + function( + name = "callAsync", + modifiers = "override", + args = "call: kotlinx.rpc.RpcCall", + typeParameters = "R", + returnType = "kotlinx.coroutines.Deferred", + ) { + code("error(\"Async calls are not supported\")") + } + } + } + + private fun FqName.toPlatformMessageType(): String { + return "${simpleName}OuterClass.${simpleName.removePrefix("$packageName.")}" + } +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt new file mode 100644 index 000000000..f31db2552 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt @@ -0,0 +1,291 @@ +/* + * Copyright 2023-2024 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.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 + +// todo parent types are broken a bit now +class ProtoToModelInterpreter( + @Suppress("unused") + private val logger: Logger, +) { + private val fileDependencies = mutableMapOf() + + fun interpretProtocRequest(message: CodeGeneratorRequest): Model { + return Model(message.protoFileList.map { it.toModel() }) + } + + // package name of a currently parsed file + private var packageName 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 = kotlinPackageName(`package`, options) + + return FileDeclaration( + name = SimpleFqName( + packageName = packageName, + simpleName = kotlinFileName(name) + ), + dependencies = dependencies, + messageDeclarations = messageTypeList.map { it.toModel() }, + enumDeclarations = enumTypeList.map { it.toModel() }, + serviceDeclarations = serviceList.map { it.toModel() }, + deprecated = options.deprecated, + doc = null, + ).also { + fileDependencies[name] = it + } + } + + private fun kotlinFileName(originalName: String): String { + return "${originalName.removeSuffix(".proto").fullProtoNameToKotlin(firstLetterUpper = true)}.kt" + } + + private fun kotlinPackageName(originalPackage: String, options: DescriptorProtos.FileOptions): String { + // todo check forbidden package names + return originalPackage + } + + private fun DescriptorProtos.DescriptorProto.toModel(): MessageDeclaration { + val fields = fieldList.mapNotNull { + val oneOfName = if (it.hasOneofIndex()) { + oneofDeclList[it.oneofIndex].name + } else { + null + } + + it.toModel(oneOfName) + } + + return MessageDeclaration( + name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), + actualFields = fields, + oneOfDeclarations = oneofDeclList.mapIndexedNotNull { i, desc -> desc.toModel(i) }, + enumDeclarations = enumTypeList.map { it.toModel() }, + nestedDeclarations = nestedTypeList.map { it.toModel() }, + deprecated = options.deprecated, + doc = null, + ) + } + + private val oneOfFieldMembers = mutableMapOf>() + + private fun DescriptorProtos.FieldDescriptorProto.toModel(oneOfName: String?): 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() + } + + oneOfFieldMembers[oneofIndex] == null -> { + oneOfFieldMembers[oneofIndex] = mutableListOf() + .also { list -> list.add(this) } + + FieldType.Reference(oneOfName.fullProtoNameToKotlin(firstLetterUpper = true).toFqName()) + } + + else -> { + oneOfFieldMembers[oneofIndex]!!.add(this) + null + } + } ?: return null + + return FieldDeclaration( + name = oneOfName.removePrefix("_").fullProtoNameToKotlin(), + type = fieldType, + nullable = true, + deprecated = options.deprecated, + doc = null, + ) + } + + return FieldDeclaration( + name = name.fullProtoNameToKotlin(), + type = fieldType(), + nullable = false, + deprecated = options.deprecated, + doc = null, + ) + } + + private fun DescriptorProtos.FieldDescriptorProto.fieldType(): FieldType { + return when { + hasTypeName() -> { + typeName + // from https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto + // if the name starts with a '.', it is fully-qualified. + .substringAfter('.') + .fullProtoNameToKotlin(firstLetterUpper = true) + .toFqName() + .let { wrapWithLabel(it) } + } + + else -> { + primitiveType() + } + } + } + + @Suppress("detekt.CyclomaticComplexMethod") + private fun DescriptorProtos.FieldDescriptorProto.primitiveType(): FieldType { + 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.wrapWithLabel(fqName: FqName): FieldType { + return when (label) { + DescriptorProtos.FieldDescriptorProto.Label.LABEL_REPEATED -> { + FieldType.List(fqName) + } + // LABEL_OPTIONAL is not actually optional in proto3. + // Actual optional is oneOf with one option and same name + else -> { + FieldType.Reference(fqName) + } + } + } + + private fun DescriptorProtos.OneofDescriptorProto.toModel(index: Int): OneOfDeclaration? { + val name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName() + + val fields = oneOfFieldMembers[index] ?: return null + return OneOfDeclaration( + name = name, + variants = fields.map { field -> + FieldDeclaration( + name = field.name.fullProtoNameToKotlin(firstLetterUpper = true), + type = field.fieldType(), + nullable = false, + deprecated = field.options.deprecated, + doc = null, + ) + } + ) + } + + private fun DescriptorProtos.EnumDescriptorProto.toModel(): EnumDeclaration { + val allowAlias = options.allowAlias + val originalEntries = mutableMapOf() + val aliases = mutableListOf() + + valueList.forEach { enumEntry -> + val original = originalEntries[enumEntry.number] + if (original != null) { + if (!allowAlias) { + error( + "Aliases are not allow 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 = enumEntry.name.toFqName(), + original = original, + deprecated = enumEntry.options.deprecated, + doc = null, + ) + ) + } else { + originalEntries[enumEntry.number] = EnumDeclaration.Entry( + name = enumEntry.name.toFqName(), + deprecated = enumEntry.options.deprecated, + doc = null, + ) + } + } + + return EnumDeclaration( + name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), + originalEntries = originalEntries.values.toList(), + aliases = aliases, + deprecated = options.deprecated, + doc = null, + ) + } + + private fun DescriptorProtos.ServiceDescriptorProto.toModel(): ServiceDeclaration { + return ServiceDeclaration( + name = name.fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), + methods = methodList.map { it.toModel() } + ) + } + + private fun DescriptorProtos.MethodDescriptorProto.toModel(): MethodDeclaration { + return MethodDeclaration( + name = name.fullProtoNameToKotlin(firstLetterUpper = false).toFqName(), + inputType = inputType + .substringAfter('.') // see typeName resolution + .fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), // no resolution for now + outputType = outputType + .substringAfter('.') // see typeName resolution + .fullProtoNameToKotlin(firstLetterUpper = true).toFqName(), // no resolution for now + 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(true)}" + } else { + simpleProtoNameToKotlin(firstLetterUpper) + } + } + + private val snakeRegExp = "_[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 String.toFqName(parent: FqName? = null) = SimpleFqName(packageName, this, parent) +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt new file mode 100644 index 000000000..456ef9e6c --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/RpcProtobufPlugin.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.FileAppender +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.Feature +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.helpers.NOPLogger + +class RpcProtobufPlugin { + companion object { + private const val DEBUG_OUTPUT_OPTION = "debugOutput" + private const val MESSAGE_MODE_OPTION = "messageMode" + } + + 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 val logger: Logger by lazy { + val debugOutput = debugOutput ?: return@lazy NOPLogger.NOP_LOGGER + + (LoggerFactory.getILoggerFactory().getLogger("RpcProtobufPlugin") as ch.qos.logback.classic.Logger).apply { + val appender = FileAppender().apply { + isAppend = true + file = debugOutput + encoder = PatternLayoutEncoder().apply { + pattern = "%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + } + } + + addAppender(appender) + + level = Level.ALL + } + } + + fun run(input: CodeGeneratorRequest): CodeGeneratorResponse { + val parameters = input.parameter.split(",").associate { + it.split("=").let { (key, value) -> key to value } + } + + debugOutput = parameters[DEBUG_OUTPUT_OPTION] + messageGenerationMode = MessageMode.of(parameters[MESSAGE_MODE_OPTION]) + + val files = input.generateKotlinFiles() + .map { file -> + CodeGeneratorResponse.File.newBuilder() + .apply { + val dir = file.packageName?.replace('.', '/')?.plus("/") ?: "" + + // some filename already contain package (true for Google's default .proto files) + val filename = file.filename?.removePrefix(dir) ?: error("File name can not be null") + name = "$dir$filename" + content = file.build() + } + .build() + } + + return CodeGeneratorResponse.newBuilder() + .apply { + files.forEach(::addFile) + + supportedFeatures = Feature.FEATURE_PROTO3_OPTIONAL_VALUE.toLong() + } + .build() + } + + private fun CodeGeneratorRequest.generateKotlinFiles(): List { + val interpreter = ProtoToModelInterpreter(logger) + val model = interpreter.interpretProtocRequest(this) + val fileGenerator = ModelToKotlinGenerator(model, logger, CodeGenerationParameters(messageGenerationMode)) + return fileGenerator.generateKotlinFiles() + } +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt new file mode 100644 index 000000000..a5a2076a8 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/EnumDeclaration.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023-2024 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 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/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt new file mode 100644 index 000000000..4fe91144f --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FieldDeclaration.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023-2024 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 FieldDeclaration( + val name: String, + val type: FieldType, + val nullable: Boolean, + val deprecated: Boolean, + val doc: String?, +) + +sealed interface FieldType { + val defaultValue: String + + data class List(val valueName: FqName) : FieldType { + override val defaultValue: String = "emptyList()" + } + + data class Map(val keyName: FqName, val valueName: FqName) : FieldType { + override val defaultValue: String = "emptyMap()" + } + + data class Reference(val value: FqName) : FieldType { + override val defaultValue: String = "null" + } + + enum class IntegralType(simpleName: String, override val defaultValue: String) : FieldType { + STRING("String", "\"\""), + BYTES("ByteArray", "byteArrayOf()"), + BOOL("Boolean", "false"), + FLOAT("Float", "0.0f"), + DOUBLE("Double", "0.0"), + INT32("Int", "0"), + INT64("Long", "0"), + UINT32("UInt", "0u"), + UINT64("ULong", "0u"), + FIXED32("UInt", "0u"), + FIXED64("ULong", "0u"), + SINT32("Int", "0"), + SINT64("Long", "0"), + SFIXED32("Int", "0"), + SFIXED64("Long", "0"); + + val fqName: FqName = SimpleFqName("kotlin", simpleName) + } +} diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt new file mode 100644 index 000000000..3e027e04e --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FileDeclaration.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2023-2024 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: FqName, + val dependencies: List, + val messageDeclarations: List, + val enumDeclarations: List, + val serviceDeclarations: List, + val deprecated: Boolean, + val doc: String?, +) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FqName.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FqName.kt new file mode 100644 index 000000000..60a05bb2b --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FqName.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.model + +interface FqName { + val packageName: String + val simpleName: String + val parentName: FqName? +} + +data class SimpleFqName( + override val packageName: String, + override val simpleName: String, + override val parentName: FqName? = null, +): FqName diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt new file mode 100644 index 000000000..ae7a3e74e --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2023-2024 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 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/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt new file mode 100644 index 000000000..1367bc026 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MethodDeclaration.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023-2024 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: FqName, + val clientStreaming: Boolean, + val serverStreaming: Boolean, + val inputType: FqName, + val outputType: FqName, +) diff --git a/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt new file mode 100644 index 000000000..89eb1a8f1 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/Model.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2024 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/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt new file mode 100644 index 000000000..bfaef90fe --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/OneOfDeclaration.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2023-2024 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/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt new file mode 100644 index 000000000..66ce1343d --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/ServiceDeclaration.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2023-2024 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/protobuf-plugin/src/test/kotlin/kotlinx/rpc/protobuf/test/TestPrimitiveService.kt b/protobuf-plugin/src/test/kotlin/kotlinx/rpc/protobuf/test/TestPrimitiveService.kt new file mode 100644 index 000000000..0b78af9af --- /dev/null +++ b/protobuf-plugin/src/test/kotlin/kotlinx/rpc/protobuf/test/TestPrimitiveService.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf.test + +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.grpc.GrpcClient +import kotlinx.rpc.grpc.GrpcServer +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 { + @Test + fun testPrimitive(): Unit = runBlocking { + val grpcClient = GrpcClient("localhost", 8080) { + usePlaintext() + } + + val grpcServer = GrpcServer(8080) { + registerService { PrimitiveServiceImpl() } + } + + grpcServer.start() + + val service = grpcClient.withService() + val result = service.Echo(AllPrimitives { + int32 = 42 + }) + + assertEquals(42, result.int32) + } +} diff --git a/protobuf-plugin/src/test/proto/all_primitives.proto b/protobuf-plugin/src/test/proto/all_primitives.proto new file mode 100644 index 000000000..10cadf9ef --- /dev/null +++ b/protobuf-plugin/src/test/proto/all_primitives.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.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/protobuf-plugin/src/test/proto/empty_deprecated.proto b/protobuf-plugin/src/test/proto/empty_deprecated.proto new file mode 100644 index 000000000..ed69217b7 --- /dev/null +++ b/protobuf-plugin/src/test/proto/empty_deprecated.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +message EmptyDeprecated { + option deprecated = true; +} diff --git a/protobuf-plugin/src/test/proto/enum.proto b/protobuf-plugin/src/test/proto/enum.proto new file mode 100644 index 000000000..2a2e2299a --- /dev/null +++ b/protobuf-plugin/src/test/proto/enum.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "google/protobuf/descriptor.proto"; +import "options.proto"; + +extend google.protobuf.EnumValueOptions { + optional Options options = 50000; +} + +enum Enum { + option allow_alias = true; + ZERO = 0; + ONE = 1; + ONE_SECOND = 1; + TWO = 2 [deprecated = true]; + THREE = 3 [ + (options).string = "three", + (options).inner.string = "inner three" + ]; +} diff --git a/protobuf-plugin/src/test/proto/example.proto b/protobuf-plugin/src/test/proto/example.proto new file mode 100644 index 000000000..284f0cc23 --- /dev/null +++ b/protobuf-plugin/src/test/proto/example.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +message Address { + string street = 1; + City city = 2; + + enum City { + ROME = 0; + BERLIN = 1; + LONDON = 2; + } +} + +message User { + int64 id = 1; + string name = 2; + bool married = 3; + repeated User friends = 4; + optional User spouse = 5; + Address address = 6; + oneof contact { + string email = 7; + string phone = 8; + } +} diff --git a/protobuf-plugin/src/test/proto/funny_types.proto b/protobuf-plugin/src/test/proto/funny_types.proto new file mode 100644 index 000000000..9c1ecef95 --- /dev/null +++ b/protobuf-plugin/src/test/proto/funny_types.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "google/protobuf/any.proto"; +import "enum.proto"; +import "all_primitives.proto"; + +message FunnyTypes { + Enum enum = 1; + optional string optString = 2; + repeated string repString = 3; + AllPrimitives reference = 4; + optional AllPrimitives optReference = 5; + google.protobuf.Any any = 6; + string deprecated = 7 [deprecated = true]; +} diff --git a/protobuf-plugin/src/test/proto/map.proto b/protobuf-plugin/src/test/proto/map.proto new file mode 100644 index 000000000..e90f4c196 --- /dev/null +++ b/protobuf-plugin/src/test/proto/map.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "one_of.proto"; + +message Map { + map primitives = 1; + map references = 2; +} diff --git a/protobuf-plugin/src/test/proto/multiple_files.proto b/protobuf-plugin/src/test/proto/multiple_files.proto new file mode 100644 index 000000000..49195a10b --- /dev/null +++ b/protobuf-plugin/src/test/proto/multiple_files.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf; + +option java_multiple_files = true; +option java_outer_classname = "MultipleFilesNewClassName"; +option java_package = "kotlinx.rpc.protobuf"; + +message Hello { + string hello = 1; +} + +message World { + string world = 1; + + message Nested { + string nested = 2; + } + + Nested nested = 3; +} + +enum Enum { + ZERO = 0; +} + diff --git a/protobuf-plugin/src/test/proto/nested.proto b/protobuf-plugin/src/test/proto/nested.proto new file mode 100644 index 000000000..8233de003 --- /dev/null +++ b/protobuf-plugin/src/test/proto/nested.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +message Nested { + message Inner1 { + message Inner11 { + Nested.Inner2.Inner21 reference21 = 1; + Nested.Inner1.Inner12 reference12 = 2; + Nested.Inner2.NestedEnum enum = 3; + } + + message Inner12 { + Inner12 recursion = 1; + } + + Inner11 inner11 = 1; + Inner12 inner22 = 2; + string string = 3; + } + + message Inner2 { + message Inner21 { + Nested.Inner1.Inner11 reference11 = 1; + Nested.Inner2.Inner22 reference22 = 2; + } + + message Inner22 { + NestedEnum enum = 1; + } + + enum NestedEnum { + ZERO = 0; + } + + Inner21 inner21 = 1; + Inner22 inner22 = 2; + string string = 3; + } + + Inner1 inner1 = 1; + Inner2 inner2 = 2; + string string = 3; + Inner2.NestedEnum enum = 4; +} diff --git a/protobuf-plugin/src/test/proto/one_of.proto b/protobuf-plugin/src/test/proto/one_of.proto new file mode 100644 index 000000000..971bb89cf --- /dev/null +++ b/protobuf-plugin/src/test/proto/one_of.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "with_comments.proto"; +import "funny_types.proto"; +import "all_primitives.proto"; + +message OneOf { + oneof primitives { + string string = 1; + int32 int32 = 2; + bool bool = 3; + } + + oneof references { + WithComments withComments = 4; + FunnyTypes funnyTypes = 5; + } + + oneof mixed { + int64 int64 = 6; + AllPrimitives allPrimitives = 7; + } + + oneof single { + bytes bytes = 8; + } +} diff --git a/protobuf-plugin/src/test/proto/options.proto b/protobuf-plugin/src/test/proto/options.proto new file mode 100644 index 000000000..9cf2d2358 --- /dev/null +++ b/protobuf-plugin/src/test/proto/options.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +message Options { + message Inner { + string string = 1; + } + + string string = 1; + Inner inner = 2; +} diff --git a/protobuf-plugin/src/test/proto/primitive_service.proto b/protobuf-plugin/src/test/proto/primitive_service.proto new file mode 100644 index 000000000..844306c38 --- /dev/null +++ b/protobuf-plugin/src/test/proto/primitive_service.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +import "all_primitives.proto"; + +service PrimitiveService { + rpc Echo(AllPrimitives) returns (AllPrimitives); +} diff --git a/protobuf-plugin/src/test/proto/with_comments.proto b/protobuf-plugin/src/test/proto/with_comments.proto new file mode 100644 index 000000000..e1a7e1323 --- /dev/null +++ b/protobuf-plugin/src/test/proto/with_comments.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package kotlinx.rpc.protobuf.test; + +// This message has comment +message WithComments { + // and this field too. + string string = 1; +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 584358ef4..52769afe2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,11 @@ dependencyResolutionManagement { includeBuild("compiler-plugin") } +includePublic(":protobuf-plugin") + +include(":grpc") +includePublic(":grpc:grpc-core") + includePublic(":bom") includePublic(":utils") diff --git a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt index c030f2372..0da97d399 100644 --- a/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt +++ b/utils/src/commonMain/kotlin/kotlinx/rpc/internal/utils/InternalRpcApi.kt @@ -9,4 +9,5 @@ package kotlinx.rpc.internal.utils level = RequiresOptIn.Level.ERROR, ) @InternalRpcApi +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY) public annotation class InternalRpcApi