diff --git a/CHANGELOG.md b/CHANGELOG.md index 41779d6e8..73f5fac1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +# 0.5.0 +> Published 11 December 2024 + +### Features 🎉 +* Update Service Descriptors Generation by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/227 +* Kotlin 2.1.0 by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/244 +* Added basic CheckedTypeAnnotation impl with compiler plugin check by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/240 +* Strict mode by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/243 + +### Breaking Changes 🔴 +* Api naming by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/236 +* Update Service Descriptors Generation by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/227 +* Added basic CheckedTypeAnnotation impl with compiler plugin check by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/240 + +### Deprecations ⚠️ +* Api naming by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/236 +* Strict mode by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/243 + +### Infra 🚧 +* Update the project structure to work with kotlin-master by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/234 +* Fixed version formatting with ENV vars by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/235 +* Fix Kotlin master compilation by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/245 +* Opt-out from annotations type safety analysis by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/246 + +### Other Changes 🧹 +* Added test for non-serializable params by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/237 +* Updated descriptor to use `RpcType` instead of `KType` directly by @Mr3zee in https://github.com/Kotlin/kotlinx-rpc/pull/239 + +**Full Changelog**: https://github.com/Kotlin/kotlinx-rpc/compare/0.4.0...0.5.0 + # 0.4.0 > Published 5 November 2024 diff --git a/build.gradle.kts b/build.gradle.kts index 2570a4f9b..ee48ba6a0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,18 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ 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) + alias(libs.plugins.protobuf) 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..bb77aff7a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.extension @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.isInterface import org.jetbrains.kotlin.ir.visitors.IrElementTransformer internal class RpcIrServiceProcessor( @@ -16,7 +17,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)) && declaration.isInterface + ) { 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..7da4de231 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen @@ -12,6 +12,14 @@ object FirRpcPredicates { annotated(RpcClassId.rpcAnnotation.asSingleFqName()) // @Rpc } + internal val rpcMeta = DeclarationPredicate.create { + metaAnnotated(RpcClassId.rpcAnnotation.asSingleFqName(), includeItself = true) + } + + 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..b1205b35d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen @@ -7,7 +7,9 @@ package kotlinx.rpc.codegen import kotlinx.rpc.codegen.common.RpcClassId import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration +import org.jetbrains.kotlin.fir.declarations.utils.isInterface import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar import org.jetbrains.kotlin.fir.extensions.FirSupertypeGenerationExtension import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider @@ -20,10 +22,14 @@ abstract class FirRpcSupertypeGeneratorAbstract( ) : FirSupertypeGenerationExtension(session) { override fun FirDeclarationPredicateRegistrar.registerPredicates() { register(FirRpcPredicates.rpc) + register(FirRpcPredicates.grpc) } override fun needTransformSupertypes(declaration: FirClassLikeDeclaration): Boolean { - return session.predicateBasedProvider.matches(FirRpcPredicates.rpc, declaration) + return session.predicateBasedProvider.matches( + predicate = FirRpcPredicates.rpc, + declaration = declaration, + ) && declaration is FirClass && declaration.isInterface } protected fun computeAdditionalSupertypesAbstract( diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt index 85afba399..e047d2e7f 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt @@ -7,9 +7,12 @@ package kotlinx.rpc.codegen import kotlinx.rpc.codegen.common.RpcClassId import org.jetbrains.kotlin.KtSourceElement import org.jetbrains.kotlin.fir.FirSession -import org.jetbrains.kotlin.fir.declarations.getAnnotationByClassId import org.jetbrains.kotlin.fir.expressions.FirAnnotation +import org.jetbrains.kotlin.fir.expressions.UnresolvedExpressionTypeAccess +import org.jetbrains.kotlin.fir.extensions.predicate.DeclarationPredicate +import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider import org.jetbrains.kotlin.fir.resolve.fullyExpandedType +import org.jetbrains.kotlin.fir.resolve.toClassLikeSymbol import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol import org.jetbrains.kotlin.fir.symbols.SymbolInternals import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol @@ -23,16 +26,21 @@ fun FirClassSymbol<*>.isRemoteService(session: FirSession): Boolean = resolvedSu it.doesMatchesClassId(session, RpcClassId.remoteServiceInterface) } -fun FirBasedSymbol<*>.rpcAnnotationSource(session: FirSession): KtSourceElement? { - return rpcAnnotation(session)?.source +fun FirBasedSymbol<*>.rpcAnnotationSource(session: FirSession, predicate: DeclarationPredicate): KtSourceElement? { + return rpcAnnotation(session, predicate)?.source } -fun FirBasedSymbol<*>.rpcAnnotation(session: FirSession): FirAnnotation? { - return resolvedCompilerAnnotationsWithClassIds.rpcAnnotation(session) +fun FirBasedSymbol<*>.rpcAnnotation(session: FirSession, predicate: DeclarationPredicate): FirAnnotation? { + return resolvedCompilerAnnotationsWithClassIds.rpcAnnotation(session, predicate) } -fun List.rpcAnnotation(session: FirSession): FirAnnotation? { - return getAnnotationByClassId(RpcClassId.rpcAnnotation, session) +@OptIn(UnresolvedExpressionTypeAccess::class) +fun List.rpcAnnotation(session: FirSession, predicate: DeclarationPredicate): FirAnnotation? { + return find { + it.coneTypeOrNull?.toClassLikeSymbol(session)?.let { declaration -> + session.predicateBasedProvider.matches(predicate, declaration) + } == true + } } fun FirClassSymbol<*>.remoteServiceSupertypeSource(session: FirSession): KtSourceElement? { 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..31d32bcad 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.checkers @@ -9,7 +9,9 @@ import kotlinx.rpc.codegen.FirRpcPredicates import kotlinx.rpc.codegen.checkers.diagnostics.FirRpcDiagnostics import kotlinx.rpc.codegen.isRemoteService import kotlinx.rpc.codegen.remoteServiceSupertypeSource +import kotlinx.rpc.codegen.rpcAnnotation import kotlinx.rpc.codegen.rpcAnnotationSource +import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.diagnostics.DiagnosticReporter import org.jetbrains.kotlin.diagnostics.reportOn import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind @@ -18,6 +20,7 @@ import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirRegularClassChe import org.jetbrains.kotlin.fir.declarations.FirRegularClass import org.jetbrains.kotlin.fir.declarations.utils.isInterface import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider +import org.jetbrains.kotlin.fir.types.resolvedType class FirRpcAnnotationChecker(private val ctx: FirCheckersContext) : FirRegularClassChecker(MppCheckerKind.Common) { override fun check( @@ -26,12 +29,16 @@ class FirRpcAnnotationChecker(private val ctx: FirCheckersContext) : FirRegularC reporter: DiagnosticReporter, ) { val rpcAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.rpc, declaration) + val rpcMetaAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.rpcMeta, declaration) + val grpcAnnotated = context.session.predicateBasedProvider.matches(FirRpcPredicates.grpc, declaration) - if (!declaration.isInterface && rpcAnnotated) { + if (!declaration.isInterface && declaration.classKind != ClassKind.ANNOTATION_CLASS && rpcMetaAnnotated) { reporter.reportOn( - source = declaration.symbol.rpcAnnotationSource(context.session), + source = declaration.symbol.rpcAnnotationSource(context.session, FirRpcPredicates.rpcMeta), factory = FirRpcDiagnostics.WRONG_RPC_ANNOTATION_TARGET, context = context, + a = declaration.symbol.rpcAnnotation(context.session, FirRpcPredicates.rpc)?.resolvedType + ?: error("Unexpected unresolved annotation type for declaration: ${declaration.symbol.classId.asSingleFqName()}"), ) } @@ -43,9 +50,10 @@ class FirRpcAnnotationChecker(private val ctx: FirCheckersContext) : FirRegularC ) } - if (rpcAnnotated && !ctx.serializationIsPresent) { + if ((rpcAnnotated || grpcAnnotated) && !ctx.serializationIsPresent) { +// error("Serialization plugin is not present") reporter.reportOn( - source = declaration.symbol.rpcAnnotationSource(context.session), + source = declaration.symbol.rpcAnnotationSource(context.session, FirRpcPredicates.rpcMeta), factory = FirRpcDiagnostics.MISSING_SERIALIZATION_MODULE, context = context, ) diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt index dc9701e85..ab8b5e763 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.checkers.diagnostics @@ -18,8 +18,8 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry object FirRpcDiagnostics { val MISSING_RPC_ANNOTATION by error0() - val MISSING_SERIALIZATION_MODULE by warning0() - val WRONG_RPC_ANNOTATION_TARGET by error0() + val MISSING_SERIALIZATION_MODULE by error0() + val WRONG_RPC_ANNOTATION_TARGET by error1() val CHECKED_ANNOTATION_VIOLATION by error1() val NON_SUSPENDING_REQUEST_WITHOUT_STREAMING_RETURN_TYPE by error0() val AD_HOC_POLYMORPHISM_IN_RPC_SERVICE by error2() diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt index 4c6425001..20ca86bed 100644 --- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt +++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.checkers.diagnostics @@ -29,7 +29,8 @@ object RpcDiagnosticRendererFactory : BaseDiagnosticRendererFactory() { put( factory = FirRpcDiagnostics.WRONG_RPC_ANNOTATION_TARGET, - message = "@Rpc annotation is only applicable to interfaces.", + message = "@{0} annotation is only applicable to interfaces and annotation classes.", + rendererA = FirDiagnosticRenderers.RENDER_TYPE, ) put( diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt index 7c08860ba..6c123fd34 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt @@ -1,11 +1,14 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc import kotlinx.rpc.descriptor.RpcServiceDescriptor +@Deprecated("Use RpcCall instead", ReplaceWith("RpcCall"), level = DeprecationLevel.ERROR) +public typealias RPCCall = RpcCall + /** * Represents a method or field call of an RPC service. * diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt index 84ee9e252..019d66418 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc @@ -8,6 +8,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlin.coroutines.CoroutineContext +@Deprecated("Use RpcClient instead", ReplaceWith("RpcClient"), level = DeprecationLevel.ERROR) +public typealias RPCClient = RpcClient + /** * [RpcClient] represents an abstraction of an RPC client, that can handle requests from several RPC services, * transform them, send to the server and handle responses and errors. 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/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts b/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts index 5b181a9c6..78e88572a 100644 --- a/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts +++ b/gradle-conventions/src/main/kotlin/conventions-publishing.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import util.* @@ -26,7 +26,8 @@ if (isPublicModule) { fun PublishingExtension.configurePublication() { repositories { configureSonatypeRepository() - configureSpaceRepository() + configureSpaceEapRepository() + configureSpaceGrpcRepository() configureForIdeRepository() configureLocalDevRepository() } @@ -110,12 +111,21 @@ fun MavenPom.configureMavenCentralMetadata() { } } -fun RepositoryHandler.configureSpaceRepository() { +fun RepositoryHandler.configureSpaceEapRepository() { configureRepository(project) { username = "SPACE_USERNAME" password = "SPACE_PASSWORD" name = "space" - url = "https://maven.pkg.jetbrains.space/public/p/krpc/maven" + url = "https://maven.pkg.jetbrains.space/public/p/krpc/eap" + } +} + +fun RepositoryHandler.configureSpaceGrpcRepository() { + configureRepository(project) { + username = "SPACE_USERNAME" + password = "SPACE_PASSWORD" + name = "grpc" + url = "https://maven.pkg.jetbrains.space/public/p/krpc/grpc" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d6c49620e..6a70dc6d8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,9 @@ # -# Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. # distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://packages.jetbrains.team/files/p/krpc/build-deps/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://packages.jetbrains.team/files/p/krpc/build-deps/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/grpc/grpc-core/build.gradle.kts b/grpc/grpc-core/build.gradle.kts new file mode 100644 index 000000000..d8bb8c28d --- /dev/null +++ b/grpc/grpc-core/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2023-2025 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(projects.utils) + api(libs.coroutines.core) + } + } + + jvmMain { + dependencies { + implementation(libs.grpc.util) + implementation(libs.grpc.stub) + implementation(libs.grpc.protobuf) + implementation(libs.grpc.kotlin.stub) + implementation(libs.protobuf.java.util) + implementation(libs.protobuf.kotlin) + } + } + } +} diff --git a/grpc/grpc-core/gradle.properties b/grpc/grpc-core/gradle.properties new file mode 100644 index 000000000..969e394dc --- /dev/null +++ b/grpc/grpc-core/gradle.properties @@ -0,0 +1,5 @@ +# +# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +kotlinx.rpc.excludeWasmWasi=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..fb257042e --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/GrpcServer.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +import kotlinx.coroutines.SupervisorJob +import kotlinx.rpc.RpcServer +import kotlinx.rpc.descriptor.serviceDescriptorOf +import kotlinx.rpc.grpc.annotations.Grpc +import kotlinx.rpc.grpc.descriptor.GrpcServiceDescriptor +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/MutableHandlerRegistry.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.kt new file mode 100644 index 000000000..a48afdae2 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2023-2025 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 + +public expect abstract class HandlerRegistry + +@Suppress("RedundantConstructorKeyword") +public expect class MutableHandlerRegistry constructor() : HandlerRegistry { + internal fun addService(@Suppress("unused") service: ServerServiceDefinition): ServerServiceDefinition? +} 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..b53c5ab54 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Server.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023-2025 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 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/ServerServiceDefinition.kt b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.kt new file mode 100644 index 000000000..e6a91cd5e --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2025 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 + +public expect class ServerServiceDefinition 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..b9e9bd9b8 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/Status.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023-2025 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().encodeToByteArray() + } +} 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..12dec2f55 --- /dev/null +++ b/grpc/grpc-core/src/commonMain/kotlin/kotlinx/rpc/grpc/descriptor/GrpcServiceDescriptor.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc.descriptor + +import kotlinx.coroutines.Deferred +import kotlinx.rpc.RpcCall +import kotlinx.rpc.descriptor.RpcServiceDescriptor +import kotlinx.rpc.grpc.ManagedChannel +import kotlinx.rpc.grpc.ServerServiceDefinition +import kotlinx.rpc.grpc.annotations.Grpc +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/jsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.js.kt new file mode 100644 index 000000000..b0fea765e --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.js.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023-2025 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 + +public actual abstract class ManagedChannelPlatform + +public actual abstract class ManagedChannelBuilder> + +public actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + error("JS target is not supported in gRPC") +} + +public actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + error("JS target is not supported in gRPC") +} + +public actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + error("JS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.js.kt new file mode 100644 index 000000000..039271596 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.js.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2025 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.internal.utils.InternalRpcApi + +@InternalRpcApi +public actual abstract class HandlerRegistry + +@InternalRpcApi +public actual class MutableHandlerRegistry : HandlerRegistry() { + public actual fun addService(service: ServerServiceDefinition): ServerServiceDefinition? { + error("JS target is not supported in gRPC") + } +} diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/Server.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/Server.js.kt new file mode 100644 index 000000000..d4dfaf185 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/Server.js.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual abstract class ServerBuilder> { + public actual abstract fun addService(service: ServerServiceDefinition): T + + public actual abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + error("JS target is not supported in gRPC") +} + +internal actual fun Server(builder: ServerBuilder<*>): Server { + error("JS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.js.kt new file mode 100644 index 000000000..6edb959c1 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.js.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 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.internal.utils.InternalRpcApi + +@InternalRpcApi +public actual class ServerServiceDefinition diff --git a/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.js.kt b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.js.kt new file mode 100644 index 000000000..6333f1e68 --- /dev/null +++ b/grpc/grpc-core/src/jsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.js.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + error("JS target is not supported in gRPC") +} 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/MutableHandlerRegistry.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.jvm.kt new file mode 100644 index 000000000..fea15bc92 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.jvm.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2023-2025 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 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/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/ServerServiceDefinition.jvm.kt b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.jvm.kt new file mode 100644 index 000000000..632182377 --- /dev/null +++ b/grpc/grpc-core/src/jvmMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.jvm.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2025 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 + +public actual typealias ServerServiceDefinition = io.grpc.ServerServiceDefinition 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/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt new file mode 100644 index 000000000..394e07fee --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.native.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023-2025 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 + +public actual abstract class ManagedChannelPlatform + +public actual abstract class ManagedChannelBuilder> + +public actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + error("Native target is not supported in gRPC") +} + +public actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + error("Native target is not supported in gRPC") +} + +public actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + error("Native target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.native.kt new file mode 100644 index 000000000..89252fa18 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.native.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2025 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.internal.utils.InternalRpcApi + +@InternalRpcApi +public actual abstract class HandlerRegistry + +@InternalRpcApi +public actual class MutableHandlerRegistry : HandlerRegistry() { + public actual fun addService(service: ServerServiceDefinition): ServerServiceDefinition? { + error("Native target is not supported in gRPC") + } +} diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt new file mode 100644 index 000000000..6cf094071 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/Server.native.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual abstract class ServerBuilder> { + public actual abstract fun addService(service: ServerServiceDefinition): T + + public actual abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + error("Native target is not supported in gRPC") +} + +internal actual fun Server(builder: ServerBuilder<*>): Server { + error("Native target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.native.kt new file mode 100644 index 000000000..6edb959c1 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.native.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 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.internal.utils.InternalRpcApi + +@InternalRpcApi +public actual class ServerServiceDefinition diff --git a/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.native.kt b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.native.kt new file mode 100644 index 000000000..37b9a3db5 --- /dev/null +++ b/grpc/grpc-core/src/nativeMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.native.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + error("Native target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.wasmJs.kt new file mode 100644 index 000000000..5efe95cbf --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ManagedChannel.wasmJs.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023-2025 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 + +public actual abstract class ManagedChannelPlatform + +public actual abstract class ManagedChannelBuilder> + +public actual fun ManagedChannelBuilder<*>.buildChannel(): ManagedChannel { + error("WasmJS target is not supported in gRPC") +} + +public actual fun ManagedChannelBuilder(name: String, port: Int): ManagedChannelBuilder<*> { + error("WasmJS target is not supported in gRPC") +} + +public actual fun ManagedChannelBuilder(target: String): ManagedChannelBuilder<*> { + error("WasmJS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.wasmJs.kt new file mode 100644 index 000000000..a7fb017d5 --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/MutableHandlerRegistry.wasmJs.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2025 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.internal.utils.InternalRpcApi + +@InternalRpcApi +public actual abstract class HandlerRegistry + +@InternalRpcApi +public actual class MutableHandlerRegistry : HandlerRegistry() { + public actual fun addService(service: ServerServiceDefinition): ServerServiceDefinition? { + error("WasmJS target is not supported in gRPC") + } +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/Server.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/Server.wasmJs.kt new file mode 100644 index 000000000..ed0de70ee --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/Server.wasmJs.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual abstract class ServerBuilder> { + public actual abstract fun addService(service: ServerServiceDefinition): T + + public actual abstract fun fallbackHandlerRegistry(registry: HandlerRegistry?): T +} + +internal actual fun ServerBuilder(port: Int): ServerBuilder<*> { + error("WasmJS target is not supported in gRPC") +} + +internal actual fun Server(builder: ServerBuilder<*>): Server { + error("WasmJS target is not supported in gRPC") +} diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.wasmJs.kt new file mode 100644 index 000000000..6edb959c1 --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/ServerServiceDefinition.wasmJs.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023-2025 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.internal.utils.InternalRpcApi + +@InternalRpcApi +public actual class ServerServiceDefinition diff --git a/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.wasmJs.kt b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.wasmJs.kt new file mode 100644 index 000000000..b4b0137eb --- /dev/null +++ b/grpc/grpc-core/src/wasmJsMain/kotlin/kotlinx/rpc/grpc/StatusRuntimeException.wasmJs.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.grpc + +public actual fun StatusRuntimeException(status: Status): StatusRuntimeException { + error("WasmJS target is not supported in gRPC") +} diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index fe1eead6e..03189d92f 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2458,12 +2458,7 @@ wrappy@1: resolved "https://packages.jetbrains.team/npm/p/krpc/build-deps/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@8.5.0: - version "8.5.0" - resolved "https://packages.jetbrains.team/npm/p/krpc/build-deps/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" - integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== - -ws@^8.18.0: +ws@8.18.0, ws@^8.18.0: version "8.18.0" resolved "https://packages.jetbrains.team/npm/p/krpc/build-deps/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== diff --git a/krpc/krpc-client/build.gradle.kts b/krpc/krpc-client/build.gradle.kts index 39f18e9a9..2f338be67 100644 --- a/krpc/krpc-client/build.gradle.kts +++ b/krpc/krpc-client/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import util.applyAtomicfuPlugin @@ -18,6 +18,11 @@ kotlin { dependencies { api(projects.krpc.krpcCore) + // KRPC-137 Remove temporary explicit dependencies in 2.1.10 and unmute compiler tests + implementation(projects.core) + implementation(projects.utils) + implementation(projects.krpc.krpcSerialization.krpcSerializationCore) + implementation(libs.serialization.core) implementation(libs.kotlin.reflect) diff --git a/krpc/krpc-core/build.gradle.kts b/krpc/krpc-core/build.gradle.kts index 40f24dfd8..84055d510 100644 --- a/krpc/krpc-core/build.gradle.kts +++ b/krpc/krpc-core/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import util.applyAtomicfuPlugin @@ -20,6 +20,9 @@ kotlin { api(projects.krpc.krpcSerialization.krpcSerializationCore) implementation(projects.krpc.krpcLogging) + // KRPC-137 Remove temporary explicit dependencies in 2.1.10 and unmute compiler tests + implementation(projects.utils) + api(libs.coroutines.core) implementation(libs.serialization.core) implementation(libs.kotlin.reflect) diff --git a/krpc/krpc-ktor/krpc-ktor-client/build.gradle.kts b/krpc/krpc-ktor/krpc-ktor-client/build.gradle.kts index b3d2a7af1..c7aaef9a7 100644 --- a/krpc/krpc-ktor/krpc-ktor-client/build.gradle.kts +++ b/krpc/krpc-ktor/krpc-ktor-client/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ plugins { @@ -14,6 +14,10 @@ kotlin { api(projects.krpc.krpcClient) api(projects.krpc.krpcKtor.krpcKtorCore) + // KRPC-137 Remove temporary explicit dependencies in 2.1.10 and unmute compiler tests + implementation(projects.krpc.krpcCore) + implementation(projects.core) + api(libs.ktor.client.core) api(libs.ktor.client.websockets) diff --git a/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts b/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts index ff3a78cc6..b3c8081c5 100644 --- a/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts +++ b/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ plugins { @@ -14,6 +14,10 @@ kotlin { dependencies { api(projects.krpc.krpcCore) + // KRPC-137 Remove temporary explicit dependencies in 2.1.10 and unmute compiler tests + implementation(projects.core) + implementation(projects.utils) + implementation(libs.ktor.websockets) implementation(libs.coroutines.core) implementation(libs.serialization.core) diff --git a/krpc/krpc-server/build.gradle.kts b/krpc/krpc-server/build.gradle.kts index 2413cb4c3..facbe73ce 100644 --- a/krpc/krpc-server/build.gradle.kts +++ b/krpc/krpc-server/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import util.applyAtomicfuPlugin @@ -18,6 +18,11 @@ kotlin { dependencies { api(projects.krpc.krpcCore) + // KRPC-137 Remove temporary explicit dependencies in 2.1.10 and unmute compiler tests + implementation(projects.core) + implementation(projects.utils) + implementation(projects.krpc.krpcSerialization.krpcSerializationCore) + implementation(projects.krpc.krpcLogging) implementation(libs.serialization.core) diff --git a/protobuf-plugin/build.gradle.kts b/protobuf-plugin/build.gradle.kts new file mode 100644 index 000000000..a0a75d847 --- /dev/null +++ b/protobuf-plugin/build.gradle.kts @@ -0,0 +1,111 @@ +/* + * Copyright 2023-2025 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) + alias(libs.plugins.protobuf) +} + +dependencies { + implementation(libs.protobuf.java) + + implementation(libs.slf4j.api) + implementation(libs.logback.classic) + + testImplementation(projects.grpc.grpcCore) + testImplementation(libs.coroutines.core) + testImplementation(libs.kotlin.test) + + testImplementation(libs.grpc.stub) + testImplementation(libs.grpc.netty) + testImplementation(libs.grpc.protobuf) + testImplementation(libs.grpc.kotlin.stub) + testImplementation(libs.protobuf.java.util) + testImplementation(libs.protobuf.kotlin) +} + +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 + archiveClassifier = "all" + + // 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 = libs.protoc.get().toString() + } + + plugins { + create("kotlinx-rpc") { + path = "$buildDirPath/libs/protobuf-plugin-$version-all.jar" + } + + create("grpc") { + artifact = libs.grpc.protoc.gen.java.get().toString() + } + + create("grpckt") { + artifact = libs.grpc.protoc.gen.kotlin.get().toString() + ":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..c4fade1fa --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/CodeGenerator.kt @@ -0,0 +1,307 @@ +/* + * 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, + var fileOptIns: List = emptyList(), + 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 (fileOptIns.isNotEmpty()) { + appendLine("@file:OptIn(${fileOptIns.joinToString(", ")})") + newLine() + } + + var packageName = packageName + if (packageName != null && packageName.isNotEmpty()) { + 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, emptyList(), 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..35e735c0a --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ModelToKotlinGenerator.kt @@ -0,0 +1,368 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.rpc.protobuf + +import 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 + fileOptIns = listOf("ExperimentalRpcApi::class", "InternalRpcApi::class") + + dependencies.forEach { dependency -> + importPackage(dependency.name.packageName) + } + + generateDeclaredEntities(this@generateKotlinFile) + + additionalImports.forEach { + import(it) + } + import("kotlinx.rpc.internal.utils.*") + } + } + + 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.outerClassName.simpleName}.${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 + val inputType by method.inputType + val outputType by method.outputType + function( + name = method.name.simpleName, + modifiers = "suspend", + args = "message: ${inputType.name.simpleName}", + returnType = outputType.name.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.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() } + + val inputType by method.inputType + val outputType by method.outputType + + function( + name = grpcName, + modifiers = "override suspend", + args = "request: ${inputType.toPlatformMessageType()}", + returnType = 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 inputType by method.inputType + val grpcName = method.name.simpleName.replaceFirstChar { it.lowercase() } + val result = "stub.$grpcName((message as ${inputType.name.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 MessageDeclaration.toPlatformMessageType(): String { + return "${outerClassName.simpleName}.${name.simpleName.removePrefix(name.parentNameAsPrefix)}" + } +} 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..1eead0955 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/ProtoToModelInterpreter.kt @@ -0,0 +1,306 @@ +/* + * 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() + private val messages = 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(fqOuterClass()) }, + enumDeclarations = enumTypeList.map { it.toModel() }, + serviceDeclarations = serviceList.map { it.toModel() }, + deprecated = options.deprecated, + doc = null, + ).also { + fileDependencies[name] = it + } + } + + private fun DescriptorProtos.FileDescriptorProto.fqOuterClass(): FqName { + return "${name.removeSuffix(".proto").fullProtoNameToKotlin(firstLetterUpper = true)}OuterClass".toFqName() + } + + 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(outerClass: FqName): MessageDeclaration { + val fields = fieldList.mapNotNull { + val oneOfName = if (it.hasOneofIndex()) { + oneofDeclList[it.oneofIndex].name + } else { + null + } + + it.toModel(oneOfName) + } + + return MessageDeclaration( + outerClassName = outerClass, + 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(outerClass) }, + deprecated = options.deprecated, + doc = null, + ).apply { + val name = if (packageName.isEmpty()) { + name + } else { + "$packageName.$name".toFqName() + } + messages[name] = this + } + } + + 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() + .let { lazy { messages[it] ?: error("Unknown message type $it, available: ${messages.keys.joinToString(",")}") } }, + outputType = outputType + .substringAfter('.') // see typeName resolution + .fullProtoNameToKotlin(firstLetterUpper = true).toFqName() + .let { lazy { messages[it] ?: error("Unknown message type $it, available: ${messages.keys.joinToString(",")}") } }, + clientStreaming = clientStreaming, + serverStreaming = serverStreaming, + ) + } + + private fun String.fullProtoNameToKotlin(firstLetterUpper: Boolean = false): String { + val lastDelimiterIndex = indexOfLast { it == '.' || it == '/' } + return if (lastDelimiterIndex != -1) { + val packageName = substring(0, lastDelimiterIndex) + val name = substring(lastDelimiterIndex + 1) + val delimiter = this[lastDelimiterIndex] + return "$packageName$delimiter${name.simpleProtoNameToKotlin(true)}" + } else { + simpleProtoNameToKotlin(firstLetterUpper) + } + } + + private val snakeRegExp = "(_[a-z]|-[a-z])".toRegex() + + private fun String.snakeToCamelCase(): String { + return replace(snakeRegExp) { it.value.last().uppercase() } + } + + private fun String.simpleProtoNameToKotlin(firstLetterUpper: Boolean = false): String { + return snakeToCamelCase().run { + if (firstLetterUpper) { + replaceFirstChar { it.uppercase() } + } else { + this + } + } + } + + private fun 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..e5bff309b --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/FqName.kt @@ -0,0 +1,31 @@ +/* + * 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? + + val parentNameAsPrefix: String get() = parentName?.let { "$it.".removePrefix(".") } ?: "" +} + +data class SimpleFqName( + override val packageName: String, + override val simpleName: String, + override val parentName: FqName? = null, +): FqName { + override fun equals(other: Any?): Boolean { + return other is FqName && simpleName == other.simpleName + } + + override fun hashCode(): Int { + return simpleName.hashCode() + } + + override fun toString(): String { + return simpleName + } +} 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..6b1cff797 --- /dev/null +++ b/protobuf-plugin/src/main/kotlin/kotlinx/rpc/protobuf/model/MessageDeclaration.kt @@ -0,0 +1,16 @@ +/* + * 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 outerClassName: FqName, + val name: FqName, + val actualFields: List, // excludes oneOf fields, but includes oneOf itself + val oneOfDeclarations: List, + val enumDeclarations: List, + val nestedDeclarations: List, + val deprecated: Boolean, + val doc: String?, +) diff --git a/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..fe4140a25 --- /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: Lazy, + val outputType: Lazy, +) 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/image-recognizer.proto b/protobuf-plugin/src/test/proto/image-recognizer.proto new file mode 100644 index 000000000..38dde9d68 --- /dev/null +++ b/protobuf-plugin/src/test/proto/image-recognizer.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +message Image { + bytes data = 1; +} + +message RecogniseResult { + int32 category = 1; +} + +service ImageRecognizer { + rpc recognize(Image) returns (RecogniseResult); +} 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/publishLocal.sh b/publishLocal.sh index 3355b090c..4a12f6d63 100755 --- a/publishLocal.sh +++ b/publishLocal.sh @@ -7,5 +7,5 @@ set -euxo pipefail ./gradlew publishAllPublicationsToBuildRepoRepository -./gradlew -p compiler-plugin publishAllPublicationsToBuildRepoRepository -./gradlew -p gradle-plugin publishAllPublicationsToBuildRepoRepository +./gradlew -p compiler-plugin publishAllPublicationsToBuildRepoRepository --no-configuration-cache +./gradlew -p gradle-plugin publishAllPublicationsToBuildRepoRepository --no-configuration-cache diff --git a/samples/grpc-app/.gitignore b/samples/grpc-app/.gitignore new file mode 100644 index 000000000..c426c32f8 --- /dev/null +++ b/samples/grpc-app/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/samples/grpc-app/build.gradle.kts b/samples/grpc-app/build.gradle.kts new file mode 100644 index 000000000..eef5f3198 --- /dev/null +++ b/samples/grpc-app/build.gradle.kts @@ -0,0 +1,80 @@ +/* + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + kotlin("jvm") version "2.1.0" + kotlin("plugin.serialization") version "2.1.0" + id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.0-eap-grpc-1" + id("com.google.protobuf") version "0.9.4" + application +} + +group = "kotlinx.rpc.sample" +version = "0.0.1" + +application { + mainClass.set("ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(11) +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-rpc-grpc-core:0.5.0-eap-grpc-1") + + implementation("ch.qos.logback:logback-classic:1.5.16") + + implementation("io.grpc:grpc-stub:1.69.0") + implementation("io.grpc:grpc-util:1.69.0") + implementation("io.grpc:grpc-netty:1.69.0") + implementation("io.grpc:grpc-protobuf:1.69.0") + implementation("io.grpc:grpc-kotlin-stub:1.4.1") + implementation("com.google.protobuf:protobuf-java:4.29.3") + implementation("com.google.protobuf:protobuf-kotlin:4.29.3") + + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.1.0") +} + +val buildDirPath: String = project.layout.buildDirectory.get().asFile.absolutePath + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.29.3" + } + + plugins { + create("kotlinx-rpc") { + artifact = "org.jetbrains.kotlinx:kotlinx-rpc-protobuf-plugin:0.5.0-eap-grpc-1:all@jar" + } + + create("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.69.0" + } + + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" + } + } + + generateProtoTasks { + all().all { + plugins { + create("kotlinx-rpc") { + option("debugOutput=$buildDirPath/protobuf-plugin.log") + option("messageMode=interface") + } + create("grpc") + create("grpckt") + } + } + } +} diff --git a/samples/grpc-app/gradle.properties b/samples/grpc-app/gradle.properties new file mode 100644 index 000000000..27d589005 --- /dev/null +++ b/samples/grpc-app/gradle.properties @@ -0,0 +1,5 @@ +# +# Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. +# + +kotlin.code.style=official diff --git a/samples/grpc-app/gradle/wrapper/gradle-wrapper.jar b/samples/grpc-app/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..a4b76b953 Binary files /dev/null and b/samples/grpc-app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/grpc-app/gradle/wrapper/gradle-wrapper.properties b/samples/grpc-app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..cea7a793a --- /dev/null +++ b/samples/grpc-app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/grpc-app/gradlew b/samples/grpc-app/gradlew new file mode 100755 index 000000000..f3b75f3b0 --- /dev/null +++ b/samples/grpc-app/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/grpc-app/gradlew.bat b/samples/grpc-app/gradlew.bat new file mode 100644 index 000000000..9d21a2183 --- /dev/null +++ b/samples/grpc-app/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/grpc-app/settings.gradle.kts b/samples/grpc-app/settings.gradle.kts new file mode 100644 index 000000000..7090799de --- /dev/null +++ b/samples/grpc-app/settings.gradle.kts @@ -0,0 +1,5 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +rootProject.name = "grpc-app" diff --git a/samples/grpc-app/src/main/kotlin/Client.kt b/samples/grpc-app/src/main/kotlin/Client.kt new file mode 100644 index 000000000..d3c8a7066 --- /dev/null +++ b/samples/grpc-app/src/main/kotlin/Client.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.grpc.GrpcClient +import kotlinx.rpc.withService + +fun main(): Unit = runBlocking { + val grpcClient = GrpcClient("localhost", 8080) { + usePlaintext() + } + + val recognizer = grpcClient.withService() + + val image = Image { + data = byteArrayOf(0, 1, 2, 3) + } + val result = recognizer.recognize(image) + println("Recognized category: ${result.category}") + + grpcClient.cancel() +} diff --git a/samples/grpc-app/src/main/kotlin/ImageRecognizer.kt b/samples/grpc-app/src/main/kotlin/ImageRecognizer.kt new file mode 100644 index 000000000..8f085c028 --- /dev/null +++ b/samples/grpc-app/src/main/kotlin/ImageRecognizer.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +import kotlinx.coroutines.delay + +class ImageRecognizerImpl : ImageRecognizer { + override suspend fun recognize(image: Image): RecogniseResult { + val byte = image.data[0].toInt() + delay(100) // heavy processing + val result = RecogniseResult { + category = if (byte == 0) 0 else 1 + } + return result + } +} diff --git a/samples/grpc-app/src/main/kotlin/Server.kt b/samples/grpc-app/src/main/kotlin/Server.kt new file mode 100644 index 000000000..d755aa442 --- /dev/null +++ b/samples/grpc-app/src/main/kotlin/Server.kt @@ -0,0 +1,12 @@ +import kotlinx.coroutines.runBlocking +import kotlinx.rpc.grpc.GrpcServer +import kotlinx.rpc.registerService + +fun main(): Unit = runBlocking { + val grpcServer = GrpcServer(8080) { + registerService { ImageRecognizerImpl() } + } + + grpcServer.start() + grpcServer.awaitTermination() +} diff --git a/samples/grpc-app/src/main/proto/image-recognizer.proto b/samples/grpc-app/src/main/proto/image-recognizer.proto new file mode 100644 index 000000000..38dde9d68 --- /dev/null +++ b/samples/grpc-app/src/main/proto/image-recognizer.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +message Image { + bytes data = 1; +} + +message RecogniseResult { + int32 category = 1; +} + +service ImageRecognizer { + rpc recognize(Image) returns (RecogniseResult); +} diff --git a/samples/grpc-app/src/main/resources/logback.xml b/samples/grpc-app/src/main/resources/logback.xml new file mode 100644 index 000000000..4a4570334 --- /dev/null +++ b/samples/grpc-app/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/samples/ktor-all-platforms-app/.fleet/settings.json b/samples/ktor-all-platforms-app/.fleet/settings.json new file mode 100644 index 000000000..05bf50272 --- /dev/null +++ b/samples/ktor-all-platforms-app/.fleet/settings.json @@ -0,0 +1,3 @@ +{ + "gradle.autoImportOnSave": false +} \ No newline at end of file diff --git a/samples/ktor-all-platforms-app/composeApp/src/commonMain/kotlin/App.kt b/samples/ktor-all-platforms-app/composeApp/src/commonMain/kotlin/App.kt index ef6420d6c..6f63073b4 100644 --- a/samples/ktor-all-platforms-app/composeApp/src/commonMain/kotlin/App.kt +++ b/samples/ktor-all-platforms-app/composeApp/src/commonMain/kotlin/App.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import androidx.compose.animation.AnimatedVisibility @@ -14,7 +14,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import io.ktor.client.* import io.ktor.http.* -import kotlinx.rpc.krpc.ktor.client.installRPC +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json @@ -28,7 +28,7 @@ expect val DEV_SERVER_HOST: String val client by lazy { HttpClient { - installRPC() + installKrpc() } } diff --git a/samples/ktor-all-platforms-app/gradle/libs.versions.toml b/samples/ktor-all-platforms-app/gradle/libs.versions.toml index 3043bce74..7cf70fad0 100644 --- a/samples/ktor-all-platforms-app/gradle/libs.versions.toml +++ b/samples/ktor-all-platforms-app/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -kotlin = "2.0.21" +kotlin = "2.1.0" -agp = "8.7.2" +agp = "8.8.0-alpha05" android-compileSdk = "35" android-minSdk = "24" android-targetSdk = "35" @@ -12,14 +12,14 @@ androidx-core-ktx = "1.15.0" androidx-espresso-core = "3.6.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" -compose = "1.7.5" -compose-plugin = "1.7.0" +compose = "1.7.6" +compose-plugin = "1.8.0-alpha01" junit = "4.13.2" -ktor = "3.0.1" -logback = "1.5.12" -serialization = "1.7.3" -coroutines = "1.9.0" -kotlinx-rpc = "0.4.0" +ktor = "3.0.3" +logback = "1.5.16" +serialization = "1.8.0" +coroutines = "1.10.1" +kotlinx-rpc = "0.5.0" [libraries] # kotlin diff --git a/samples/ktor-all-platforms-app/gradle/wrapper/gradle-wrapper.properties b/samples/ktor-all-platforms-app/gradle/wrapper/gradle-wrapper.properties index df97d72b8..cea7a793a 100644 --- a/samples/ktor-all-platforms-app/gradle/wrapper/gradle-wrapper.properties +++ b/samples/ktor-all-platforms-app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/ktor-all-platforms-app/gradlew b/samples/ktor-all-platforms-app/gradlew index f5feea6d6..f3b75f3b0 100755 --- a/samples/ktor-all-platforms-app/gradlew +++ b/samples/ktor-all-platforms-app/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/samples/ktor-all-platforms-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt b/samples/ktor-all-platforms-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt index 9fb57c627..2e8d0f649 100644 --- a/samples/ktor-all-platforms-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt +++ b/samples/ktor-all-platforms-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.sample @@ -10,7 +10,7 @@ import io.ktor.server.application.* import io.ktor.server.netty.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.routing.* -import kotlinx.rpc.krpc.ktor.server.RPC +import kotlinx.rpc.krpc.ktor.server.Krpc import kotlinx.rpc.krpc.ktor.server.rpc import kotlinx.rpc.krpc.serialization.json.json @@ -18,7 +18,7 @@ fun main(args: Array): Unit = EngineMain.main(args) @Suppress("unused") fun Application.module() { - install(RPC) + install(Krpc) installCORS() diff --git a/samples/ktor-all-platforms-app/server/src/test/kotlin/kotlinx/rpc/sample/ApplicationTest.kt b/samples/ktor-all-platforms-app/server/src/test/kotlin/kotlinx/rpc/sample/ApplicationTest.kt index 7755a8551..80440fd8f 100644 --- a/samples/ktor-all-platforms-app/server/src/test/kotlin/kotlinx/rpc/sample/ApplicationTest.kt +++ b/samples/ktor-all-platforms-app/server/src/test/kotlin/kotlinx/rpc/sample/ApplicationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.sample @@ -8,7 +8,7 @@ import UserData import UserService import io.ktor.server.testing.* import kotlinx.coroutines.flow.toList -import kotlinx.rpc.krpc.ktor.client.installRPC +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json @@ -21,7 +21,7 @@ class ApplicationTest { @Test fun testRoot() = testApplication { val service = createClient { - installRPC() + installKrpc() }.rpc("/api") { rpcConfig { serialization { diff --git a/samples/ktor-all-platforms-app/settings.gradle.kts b/samples/ktor-all-platforms-app/settings.gradle.kts index 124e4e40d..a05d83951 100644 --- a/samples/ktor-all-platforms-app/settings.gradle.kts +++ b/samples/ktor-all-platforms-app/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ @file:Suppress("UnstableApiUsage") diff --git a/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/data/Client.kt b/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/data/Client.kt index e6aeded96..01a658e50 100644 --- a/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/data/Client.kt +++ b/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/data/Client.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.sample.data @@ -7,15 +7,15 @@ package kotlinx.rpc.sample.data import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.request.url -import kotlinx.rpc.RPCClient -import kotlinx.rpc.krpc.ktor.client.installRPC +import kotlinx.rpc.RpcClient +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json -suspend fun createRpcClient(): RPCClient { +suspend fun createRpcClient(): RpcClient { return HttpClient(OkHttp) { - installRPC() + installKrpc() }.rpc { url("ws://10.0.2.2:8080/api") diff --git a/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/ui/AppViewModel.kt b/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/ui/AppViewModel.kt index 9eb7679d5..43ee0fc08 100644 --- a/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/ui/AppViewModel.kt +++ b/samples/ktor-android-app/app/src/main/kotlin/kotlinx/rpc/sample/ui/AppViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.sample.ui @@ -14,14 +14,14 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import kotlinx.rpc.RPCClient +import kotlinx.rpc.RpcClient import kotlinx.rpc.krpc.streamScoped import kotlinx.rpc.withService import kotlinx.rpc.sample.MyService import kotlinx.rpc.sample.UserData class AppViewModel : ViewModel() { - private var rpcClient: RPCClient? = null + private var rpcClient: RpcClient? = null private var apiService: MyService? = null private val _uiState = MutableStateFlow(null) @@ -64,4 +64,4 @@ class AppViewModel : ViewModel() { } } } -} \ No newline at end of file +} diff --git a/samples/ktor-android-app/gradle/libs.versions.toml b/samples/ktor-android-app/gradle/libs.versions.toml index 37383d37c..f59b98e5e 100644 --- a/samples/ktor-android-app/gradle/libs.versions.toml +++ b/samples/ktor-android-app/gradle/libs.versions.toml @@ -1,21 +1,21 @@ [versions] -agp = "8.7.2" -kotlin = "2.0.21" +agp = "8.8.0" +kotlin = "2.1.0" androidx-activityCompose = "1.9.3" androidx-appcompat = "1.7.0" androidx-constraintlayout = "2.2.0" androidx-core-ktx = "1.15.0" androidx-test-junit = "1.2.1" -compose = "1.7.5" +compose = "1.7.6" compose-plugin = "1.5.14" # https://mvnrepository.com/artifact/androidx.compose.compiler/compiler -compose-bom = "2024.10.01" +compose-bom = "2024.12.01" material3 = "1.3.1" junit = "4.13.2" -ktor = "3.0.1" -kotlinx-serialization-json = "1.7.3" -kotlinx-coroutines-core = "1.9.0" -logback = "1.5.12" -kotlinx-rpc = "0.4.0" +ktor = "3.0.3" +kotlinx-serialization-json = "1.8.0" +kotlinx-coroutines-core = "1.10.1" +logback = "1.5.16" +kotlinx-rpc = "0.5.0" [libraries] # kotlin diff --git a/samples/ktor-android-app/gradle/wrapper/gradle-wrapper.properties b/samples/ktor-android-app/gradle/wrapper/gradle-wrapper.properties index df97d72b8..cea7a793a 100644 --- a/samples/ktor-android-app/gradle/wrapper/gradle-wrapper.properties +++ b/samples/ktor-android-app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/ktor-android-app/gradlew b/samples/ktor-android-app/gradlew index f5feea6d6..f3b75f3b0 100755 --- a/samples/ktor-android-app/gradlew +++ b/samples/ktor-android-app/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/samples/ktor-android-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt b/samples/ktor-android-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt index d7a9370a4..1ac0a6929 100644 --- a/samples/ktor-android-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt +++ b/samples/ktor-android-app/server/src/main/kotlin/kotlinx/rpc/sample/Application.kt @@ -1,12 +1,12 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.sample import io.ktor.server.application.* import io.ktor.server.cio.* import io.ktor.server.routing.* -import io.ktor.server.websocket.* +import kotlinx.rpc.krpc.ktor.server.Krpc import kotlinx.rpc.krpc.ktor.server.rpc import kotlinx.rpc.krpc.serialization.json.json @@ -14,7 +14,7 @@ fun main(args: Array): Unit = EngineMain.main(args) @Suppress("unused") fun Application.module() { - install(WebSockets) + install(Krpc) routing { rpc("/api") { diff --git a/samples/ktor-android-app/server/src/test/kotlin/ApplicationTest.kt b/samples/ktor-android-app/server/src/test/kotlin/ApplicationTest.kt index 1c76842c1..a706b3b6b 100644 --- a/samples/ktor-android-app/server/src/test/kotlin/ApplicationTest.kt +++ b/samples/ktor-android-app/server/src/test/kotlin/ApplicationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import io.ktor.client.request.* @@ -9,7 +9,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* import kotlinx.coroutines.flow.toList -import kotlinx.rpc.krpc.ktor.client.installRPC +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json @@ -24,7 +24,7 @@ class ApplicationTest { @Test fun testRoot() = testApplication { val service = createClient { - installRPC() + installKrpc() }.rpc("/api") { rpcConfig { serialization { diff --git a/samples/ktor-web-app/README.md b/samples/ktor-web-app/README.md index fe9243f8e..e19bf290e 100644 --- a/samples/ktor-web-app/README.md +++ b/samples/ktor-web-app/README.md @@ -1,9 +1,6 @@ # Ktor Web App Sample application with Kotlin/Js on frontend and Kotlin/Jvm on backend,that uses kRPC with Ktor to communicate. -> Warning: the code is broken due to https://youtrack.jetbrains.com/issue/KT-71757/ -> No workarounds for now - ### Running frontend To run frontend in development mode, run this command: ```bash diff --git a/samples/ktor-web-app/frontend/build.gradle.kts b/samples/ktor-web-app/frontend/build.gradle.kts index 51a5e440f..5a975aad2 100644 --- a/samples/ktor-web-app/frontend/build.gradle.kts +++ b/samples/ktor-web-app/frontend/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { implementation(libs.kotlin.stdlib.js) implementation(libs.ktor.client.js) implementation(libs.ktor.client.websockets.js) + implementation(libs.kotlinx.rpc.core) implementation(libs.kotlinx.rpc.krpc.ktor.client) implementation(libs.kotlinx.rpc.krpc.serialization.json) diff --git a/samples/ktor-web-app/frontend/src/jsMain/kotlin/App.kt b/samples/ktor-web-app/frontend/src/jsMain/kotlin/App.kt index bec029654..ce8284fa1 100644 --- a/samples/ktor-web-app/frontend/src/jsMain/kotlin/App.kt +++ b/samples/ktor-web-app/frontend/src/jsMain/kotlin/App.kt @@ -2,7 +2,7 @@ * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ -import kotlinx.rpc.RPCClient +import kotlinx.rpc.RpcClient import kotlinx.rpc.withService import react.FC import react.Props @@ -11,7 +11,7 @@ import react.useEffectOnce import react.useState val App = FC { - var rpcClient by useState(null) + var rpcClient by useState(null) useEffectOnce { rpcClient = initRpcClient() diff --git a/samples/ktor-web-app/frontend/src/jsMain/kotlin/RPC.kt b/samples/ktor-web-app/frontend/src/jsMain/kotlin/RPC.kt index 3205500f8..1330b72d2 100644 --- a/samples/ktor-web-app/frontend/src/jsMain/kotlin/RPC.kt +++ b/samples/ktor-web-app/frontend/src/jsMain/kotlin/RPC.kt @@ -5,15 +5,15 @@ import io.ktor.client.* import io.ktor.client.engine.js.* import io.ktor.client.request.* -import kotlinx.rpc.RPCClient -import kotlinx.rpc.krpc.ktor.client.installRPC +import kotlinx.rpc.RpcClient +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json -suspend fun initRpcClient(): RPCClient { +suspend fun initRpcClient(): RpcClient { return HttpClient(Js) { - installRPC() + installKrpc() }.rpc { url("ws://localhost:8080/api") diff --git a/samples/ktor-web-app/gradle/libs.versions.toml b/samples/ktor-web-app/gradle/libs.versions.toml index 7f245a080..a15bf2a42 100644 --- a/samples/ktor-web-app/gradle/libs.versions.toml +++ b/samples/ktor-web-app/gradle/libs.versions.toml @@ -1,11 +1,11 @@ [versions] -kotlin = "2.0.21" -kotlin-wrappers-bom = "1.0.0-pre.823" -ktor = "3.0.1" -kotlinx-serialization-json = "1.7.3" -kotlinx-coroutines-core = "1.9.0" -logback = "1.5.12" -kotlinx-rpc = "0.4.0" +kotlin = "2.1.0" +kotlin-wrappers-bom = "2025.1.2" +ktor = "3.0.3" +kotlinx-serialization-json = "1.8.0" +kotlinx-coroutines-core = "1.10.1" +logback = "1.5.16" +kotlinx-rpc = "0.5.0" [libraries] # kotlin diff --git a/samples/ktor-web-app/gradle/wrapper/gradle-wrapper.properties b/samples/ktor-web-app/gradle/wrapper/gradle-wrapper.properties index df97d72b8..cea7a793a 100644 --- a/samples/ktor-web-app/gradle/wrapper/gradle-wrapper.properties +++ b/samples/ktor-web-app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/ktor-web-app/gradlew b/samples/ktor-web-app/gradlew index f5feea6d6..f3b75f3b0 100755 --- a/samples/ktor-web-app/gradlew +++ b/samples/ktor-web-app/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/samples/ktor-web-app/server/src/main/kotlin/Application.kt b/samples/ktor-web-app/server/src/main/kotlin/Application.kt index 7b859168d..13b1580cf 100644 --- a/samples/ktor-web-app/server/src/main/kotlin/Application.kt +++ b/samples/ktor-web-app/server/src/main/kotlin/Application.kt @@ -8,7 +8,7 @@ import io.ktor.server.cio.* import io.ktor.server.http.content.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.routing.* -import kotlinx.rpc.krpc.ktor.server.RPC +import kotlinx.rpc.krpc.ktor.server.Krpc import kotlinx.rpc.krpc.ktor.server.rpc import kotlinx.rpc.krpc.serialization.json.json @@ -16,7 +16,7 @@ fun main(args: Array): Unit = EngineMain.main(args) @Suppress("unused") fun Application.module() { - install(RPC) + install(Krpc) installCORS() diff --git a/samples/ktor-web-app/server/src/test/kotlin/ApplicationTest.kt b/samples/ktor-web-app/server/src/test/kotlin/ApplicationTest.kt index a11982f4d..837f60027 100644 --- a/samples/ktor-web-app/server/src/test/kotlin/ApplicationTest.kt +++ b/samples/ktor-web-app/server/src/test/kotlin/ApplicationTest.kt @@ -1,10 +1,10 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import io.ktor.server.testing.* import kotlinx.coroutines.flow.toList -import kotlinx.rpc.krpc.ktor.client.installRPC +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json @@ -21,7 +21,7 @@ class ApplicationTest { } val service = createClient { - installRPC() + installKrpc() }.rpc("/api") { rpcConfig { serialization { diff --git a/samples/simple-ktor-app/build.gradle.kts b/samples/simple-ktor-app/build.gradle.kts index d0c86a58c..00cff85e5 100644 --- a/samples/simple-ktor-app/build.gradle.kts +++ b/samples/simple-ktor-app/build.gradle.kts @@ -1,12 +1,12 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ plugins { - kotlin("jvm") version "2.0.21" - kotlin("plugin.serialization") version "2.0.21" - id("io.ktor.plugin") version "3.0.1" - id("org.jetbrains.kotlinx.rpc.plugin") version "0.4.0" + kotlin("jvm") version "2.1.0" + kotlin("plugin.serialization") version "2.1.0" + id("io.ktor.plugin") version "3.0.3" + id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.0" } group = "kotlinx.rpc.sample" @@ -28,17 +28,17 @@ kotlin { } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.0") - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server:0.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server:0.5.0") implementation("io.ktor:ktor-client-cio") implementation("io.ktor:ktor-server-netty-jvm") - implementation("ch.qos.logback:logback-classic:1.5.12") + implementation("ch.qos.logback:logback-classic:1.5.16") testImplementation("io.ktor:ktor-server-test-host") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.0.10") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.1.0") } diff --git a/samples/simple-ktor-app/gradle/wrapper/gradle-wrapper.properties b/samples/simple-ktor-app/gradle/wrapper/gradle-wrapper.properties index df97d72b8..cea7a793a 100644 --- a/samples/simple-ktor-app/gradle/wrapper/gradle-wrapper.properties +++ b/samples/simple-ktor-app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/samples/simple-ktor-app/gradlew b/samples/simple-ktor-app/gradlew index f5feea6d6..f3b75f3b0 100755 --- a/samples/simple-ktor-app/gradlew +++ b/samples/simple-ktor-app/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/samples/simple-ktor-app/src/main/kotlin/Client.kt b/samples/simple-ktor-app/src/main/kotlin/Client.kt index 284847855..b00acb2e1 100644 --- a/samples/simple-ktor-app/src/main/kotlin/Client.kt +++ b/samples/simple-ktor-app/src/main/kotlin/Client.kt @@ -3,12 +3,12 @@ */ import io.ktor.client.* -import io.ktor.client.plugins.websocket.* import io.ktor.http.* import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json @@ -17,7 +17,7 @@ import kotlinx.rpc.withService fun main() = runBlocking { val ktorClient = HttpClient { - install(WebSockets) + installKrpc() } val client = ktorClient.rpc { @@ -37,8 +37,10 @@ fun main() = runBlocking { val recognizer: ImageRecognizer = client.withService() val stateJob = launch { - recognizer.currentlyProcessedImage.collect { - println("New state, current image: $it") + streamScoped { + recognizer.currentlyProcessedImage().collect { + println("New state, current image: $it") + } } } diff --git a/samples/simple-ktor-app/src/main/kotlin/ImageRecognizer.kt b/samples/simple-ktor-app/src/main/kotlin/ImageRecognizer.kt index 57a3df5e0..df7c4ea93 100644 --- a/samples/simple-ktor-app/src/main/kotlin/ImageRecognizer.kt +++ b/samples/simple-ktor-app/src/main/kotlin/ImageRecognizer.kt @@ -5,10 +5,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.rpc.RemoteService -import kotlinx.rpc.RPCEagerField import kotlinx.rpc.annotations.Rpc import kotlinx.serialization.Serializable import kotlin.coroutines.CoroutineContext @@ -28,8 +27,7 @@ enum class Category { @Rpc interface ImageRecognizer : RemoteService { - @RPCEagerField - val currentlyProcessedImage: StateFlow + suspend fun currentlyProcessedImage(): Flow suspend fun recognize(image: Image): Category @@ -37,7 +35,13 @@ interface ImageRecognizer : RemoteService { } class ImageRecognizerService(override val coroutineContext: CoroutineContext) : ImageRecognizer { - override val currentlyProcessedImage: MutableStateFlow = MutableStateFlow(null) + private val currentlyProcessedImage: MutableStateFlow = MutableStateFlow(null) + + override suspend fun currentlyProcessedImage(): Flow { + return flow { + currentlyProcessedImage.collect { emit(it) } + } + } override suspend fun recognize(image: Image): Category { currentlyProcessedImage.value = image diff --git a/samples/simple-ktor-app/src/main/kotlin/Server.kt b/samples/simple-ktor-app/src/main/kotlin/Server.kt index c5fa1ae91..b8fb73b34 100644 --- a/samples/simple-ktor-app/src/main/kotlin/Server.kt +++ b/samples/simple-ktor-app/src/main/kotlin/Server.kt @@ -6,7 +6,7 @@ import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.routing.* -import kotlinx.rpc.krpc.ktor.server.RPC +import kotlinx.rpc.krpc.ktor.server.Krpc import kotlinx.rpc.krpc.ktor.server.rpc import kotlinx.rpc.krpc.serialization.json.json @@ -17,7 +17,7 @@ fun main() { } fun Application.module() { - install(RPC) + install(Krpc) routing { rpc("/image-recognizer") { diff --git a/samples/simple-ktor-app/src/test/kotlin/ApplicationTest.kt b/samples/simple-ktor-app/src/test/kotlin/ApplicationTest.kt index 2b754f0a8..8558cded5 100644 --- a/samples/simple-ktor-app/src/test/kotlin/ApplicationTest.kt +++ b/samples/simple-ktor-app/src/test/kotlin/ApplicationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ import io.ktor.server.testing.* @@ -8,10 +8,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.rpc.awaitFieldInitialization +import kotlinx.rpc.krpc.ktor.client.installKrpc import kotlinx.rpc.krpc.ktor.client.installRPC import kotlinx.rpc.krpc.ktor.client.rpc import kotlinx.rpc.krpc.ktor.client.rpcConfig import kotlinx.rpc.krpc.serialization.json.json +import kotlinx.rpc.krpc.streamScoped import kotlinx.rpc.withService import org.junit.Test import kotlin.test.assertContentEquals @@ -26,7 +28,7 @@ class ApplicationTest { } val rpcClient = createClient { - installRPC() + installKrpc() }.rpc("/image-recognizer") { rpcConfig { serialization { @@ -42,12 +44,14 @@ class ApplicationTest { assertEquals(null, recognizer.awaitFieldInitialization { currentlyProcessedImage }.value) val job = CoroutineScope(Dispatchers.IO).launch { - recognizer.currentlyProcessedImage.collect { image -> - val stringValue = image?.data?.toHexString() - flowList.add(stringValue) + streamScoped { + recognizer.currentlyProcessedImage().collect { image -> + val stringValue = image?.data?.toHexString() + flowList.add(stringValue) - if (stringValue == "000203") { - coroutineContext.cancel() + if (stringValue == "000203") { + coroutineContext.cancel() + } } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 584358ef4..ef8a5687b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,13 +27,18 @@ plugins { id("conventions-repositories") id("conventions-version-resolution") id("conventions-develocity") - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" } dependencyResolutionManagement { includeBuild("compiler-plugin") } +includePublic(":protobuf-plugin") + +include(":grpc") +includePublic(":grpc:grpc-core") + includePublic(":bom") includePublic(":utils") diff --git a/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java b/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java index 03f61a0be..ea1dbe626 100644 --- a/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java +++ b/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/BoxTestGenerated.java @@ -1,7 +1,7 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.test.runners; @@ -10,6 +10,8 @@ import org.jetbrains.kotlin.test.util.KtTestUtil; import org.jetbrains.kotlin.test.TargetBackend; import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.Ignore; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.File; @@ -19,6 +21,7 @@ @SuppressWarnings("all") @TestMetadata("src/testData/box") @TestDataPath("$PROJECT_ROOT") +@Disabled("KRPC-137") public class BoxTestGenerated extends AbstractBoxTest { @Test public void testAllFilesPresentInBox() { diff --git a/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/DiagnosticTestGenerated.java b/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/DiagnosticTestGenerated.java index 995639c62..94cdb9319 100644 --- a/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/DiagnosticTestGenerated.java +++ b/tests/compiler-plugin-tests/src/test-gen/kotlinx/rpc/codegen/test/runners/DiagnosticTestGenerated.java @@ -1,7 +1,7 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.test.runners; @@ -9,8 +9,8 @@ import com.intellij.testFramework.TestDataPath; import org.jetbrains.kotlin.test.util.KtTestUtil; import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; - import java.io.File; import java.util.regex.Pattern; @@ -18,6 +18,7 @@ @SuppressWarnings("all") @TestMetadata("src/testData/diagnostics") @TestDataPath("$PROJECT_ROOT") +@Disabled("KRPC-137") public class DiagnosticTestGenerated extends AbstractDiagnosticTest { @Test public void testAllFilesPresentInDiagnostics() { diff --git a/tests/compiler-plugin-tests/src/test/kotlin/kotlinx/rpc/codegen/test/GenerateTests.kt b/tests/compiler-plugin-tests/src/test/kotlin/kotlinx/rpc/codegen/test/GenerateTests.kt index 12eb91624..7aba04105 100644 --- a/tests/compiler-plugin-tests/src/test/kotlin/kotlinx/rpc/codegen/test/GenerateTests.kt +++ b/tests/compiler-plugin-tests/src/test/kotlin/kotlinx/rpc/codegen/test/GenerateTests.kt @@ -1,23 +1,22 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.codegen.test -import kotlinx.rpc.codegen.test.runners.AbstractBoxTest -import kotlinx.rpc.codegen.test.runners.AbstractDiagnosticTest import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5 fun main() { generateTestGroupSuiteWithJUnit5 { testGroup(testDataRoot = "src/testData", testsRoot = "src/test-gen") { - testClass { - model("diagnostics") - } + // KRPC-137 Remove temporary explicit dependencies in 2.1.10 and unmute compiler tests +// testClass { +// model("diagnostics") +// } - testClass { - model("box") - } +// testClass { +// model("box") +// } } } } diff --git a/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.kt b/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.kt index 51efbb45e..a478a8309 100644 --- a/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.kt +++ b/tests/compiler-plugin-tests/src/testData/diagnostics/rpcChecked.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ @file:OptIn(ExperimentalRpcApi::class) @@ -61,3 +61,15 @@ inline suspend fun fail(client: RpcClient, server: RpcServer, serviceDescriptorOf<NotAService>() serviceDescriptorOf<T>() } + +@Rpc +annotation class Grpc + +@Grpc +interface MyGrpcService + +@Grpc +class WrongGrpcTarget + +@Rpc +class WrongRpcTarget 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 diff --git a/versions-root/kotlin-versions-lookup.csv b/versions-root/kotlin-versions-lookup.csv index cc9448d6f..4695aab06 100644 --- a/versions-root/kotlin-versions-lookup.csv +++ b/versions-root/kotlin-versions-lookup.csv @@ -1,5 +1,5 @@ Kotlin,atomicfu,serialization,detekt-gradle-plugin,gradle-kotlin-dsl,binary-compatibility-validator,kover -2.1.0,0.26.0,1.7.3,1.23.7,5.1.2,0.16.3,0.8.3 +2.1.0,0.27.0,1.8.0,1.23.7,5.1.2,0.17.0,0.9.1 2.0.21,0.26.0,1.7.3,1.23.7,5.1.2,0.16.3,0.8.3 2.0.20,0.26.0,1.7.3,1.23.7,5.1.2,0.16.3,0.8.3 2.0.10,0.26.0,1.7.1,1.23.7,5.1.2,0.16.3,0.8.3 diff --git a/versions-root/libs.versions.toml b/versions-root/libs.versions.toml index 35e36659c..185946c0e 100644 --- a/versions-root/libs.versions.toml +++ b/versions-root/libs.versions.toml @@ -8,19 +8,23 @@ kotlin-compiler = "0.0.0" # default to kotlin-lang or env.KOTLIN_COMPILER_VERSIO # kotlin independent versions detekt-analyzer = "1.23.6" -coroutines = "1.9.0" -ktor = "3.0.1" -kotlin-logging = "7.0.0" +coroutines = "1.10.1" +ktor = "3.0.3" +kotlin-logging = "7.0.3" slf4j = "2.0.16" logback = "1.3.14" gradle-plugin-publish = "1.3.0" -kotlin-wrappers = "1.0.0-pre.823" +kotlin-wrappers = "2025.1.2" junit4 = "4.13.2" -junit5 = "5.11.3" +junit5 = "5.11.4" intellij = "233.13135.128" gradle-doctor = "0.10.0" -kotlinx-browser = "0.2" -shadow-jar = "9.0.0-beta2" +kotlinx-browser = "0.3" +shadow-jar = "9.0.0-beta4" +grpc = "1.69.0" +grpc-kotlin = "1.4.1" +protobuf = "4.29.3" +protobuf-gradle = "0.9.4" # Stub versions – relpaced based on kotlin, mostly for gradle-related (plugins) dependencies # but also for dependencies for compiler-specific modules. @@ -28,12 +32,12 @@ shadow-jar = "9.0.0-beta2" # The current version is the one that is used with the latest Kotlin. # # NOTE: When updating kotlin-versions-lookup.csv, update the latest version here for the Renovate configs -atomicfu = "0.26.0" -serialization = "1.7.3" +atomicfu = "0.27.0" +serialization = "1.8.0" detekt-gradle-plugin = "1.23.7" -gradle-kotlin-dsl = "5.1.2" -binary-compatibility-validator = "0.16.3" -kover = "0.8.3" +gradle-kotlin-dsl = "5.2.0" +binary-compatibility-validator = "0.17.0" +kover = "0.9.1" [libraries] # kotlinx.rpc – references to the included builds @@ -87,6 +91,19 @@ junit5-platform-launcher = { module = "org.junit.platform:junit-platform-launche junit5-platform-runner = { module = "org.junit.platform:junit-platform-runner" } junit5-platform-suite-api = { module = "org.junit.platform:junit-platform-suite-api" } +# grpc and protobuf +protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } +protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } +protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } +protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } +grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" } +grpc-util = { module = "io.grpc:grpc-util", version.ref = "grpc" } +grpc-netty = { module = "io.grpc:grpc-netty", version.ref = "grpc" } +grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc" } +grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpc-kotlin" } +grpc-protoc-gen-java = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc" } +grpc-protoc-gen-kotlin = { module = "io.grpc:protoc-gen-grpc-kotlin", version.ref = "grpc-kotlin" } + # other kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "kotlin-logging" } kotlin-logging-legacy = { module = "io.github.microutils:kotlin-logging", version.ref = "kotlin-logging" } @@ -114,6 +131,7 @@ gradle-kotlin-dsl = { id = "org.gradle.kotlin.kotlin-dsl", version.ref = "gradle kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } gradle-plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "gradle-plugin-publish" } shadow-jar = { id = "com.gradleup.shadow", version.ref = "shadow-jar" } +protobuf = { id = "com.google.protobuf", version.ref = "protobuf-gradle" } # gradle-conventions project conventions-common = { id = "conventions-common", version.ref = "kotlinx-rpc" } @@ -126,4 +144,4 @@ conventions-npm = { id = "conventions-npm", version.ref = "kotlinx-rpc" } compiler-specific-module = { id = "compiler-specific-module", version.ref = "kotlinx-rpc" } # gradle-plugin project -kotlinx-rpc = { id = "org.jetbrains.kotlinx.rpc.plugin", version.ref = "kotlinx-rpc" } +kotlinx-rpc = { id = "org.jetbrains.kotlinx.rpc.plugin" }