Kroto+
Code generator for bringing together Kotlin, Protobuf, Coroutines, and gRPC
- Getting Started With Gradle
- Stub Rpc Method Overloads
- Rpc Method Coroutine Support
- Mock Service Generator
- Message Builder Lambda Generator
- User Defined External Generators
Code Generators
- There are several built in code generators that each accept unique configuration options.
- There is also preliminary support for registering custom external code generators. The api for doing so will be documented in the near future and accompanied by an example project.
Stub Rpc Method Overloads
This modules generates extension methods that overload the request message argument for rpc methods with a builder lambda block.
//Original Java-Style builders
val response = serviceStub.myRpcMethod(ExampleServiceGrpc.MyRpcMethodRequest
.newBuilder()
.setId(100)
.setName("some name")
.build())
//Kroto+ Overloaded
val response = serviceStub.myRpcMethod{
id = 100
name = "some name"
}For rpc methods with a request type of com.google.protobuf.Empty then a no args overload is supplied.
//Original
val response = serviceStub.myRpcMethod(Empty.getDefaultInstance())
//Kroto+ Overloaded
val response = serviceStub.myRpcMethod()For unary rpc methods, the stub overload generator will create the following extensions
//If request type is Empty
inline fun ExampleServiceStub.myRpcMethod(): ExampleServiceGrpc.MyRpcMethodResponse =
myRpcMethod(Empty.getDefaultInstace())
//Otherwise
//Future Stub
inline fun ExampleServiceFutureStub.myRpcMethod(block: ExampleServiceGrpc.MyRpcMethodRequest.Builder.() -> Unit): ListenableFuture<ExampleServiceGrpc.MyRpcMethodResponse> {
val request = ExampleServiceGrpc.MyRpcMethodRequest.newBuilder().apply(block).build()
return myRpcMethod(request)
}
//BlockingStub
inline fun ExampleServiceBlockingStub.myRpcMethod(block: ExampleServiceGrpc.MyRpcMethodRequest.Builder.() -> Unit): ExampleServiceGrpc.MyRpcMethodResponse {
val request = ExampleServiceGrpc.MyRpcMethodRequest.newBuilder().apply(block).build()
return myRpcMethod(request)
}
Coroutine Support
In addition to request message arguments as builder lambda rpc overloads, this module can also generate suspending overloads for rpc calls. This allows blocking style rpc calls without the use of the blocking stub, preventing any negative impact on coroutine performance.
- This is accomplished by defining extension functions for async service stubs and combining a response observer with a coroutine builder.
- This option requires the artifact
kroto-plus-coroutinesas a dependency. This artifact is small and only consists of the bridging support for response observer to coroutine. - If your code relies on thread local objects, such as those stored in
io.grpc.Contextthen extra care needs to be taken to ensure these objects will be reattached via aContinuationInterceptor. Thekroto-plus-coroutinesartifact will provide support for this in the next release.
//Async Stub
suspend fun ExampleServiceStub.myRpcMethod(request: ExampleServiceGrpc.MyRpcMethodRequest): ExampleServiceGrpc.MyRpcMethodResponse =
suspendingUnaryCallObserver{ observer -> myRpcMethod(request,observer) }
suspend inline fun ExampleServiceStub.myRpcMethod(block: ExampleServiceGrpc.MyRpcMethodRequest.Builder.() -> Unit): ExampleServiceGrpc.MyRpcMethodResponse {
val request = ExampleServiceGrpc.MyRpcMethodRequest.newBuilder().apply(block).build()
return myRpcMethod(request)
}There are also overloads generated for bridging Client, Server, and Bidirectional streaming methods with coroutine Channels
The included example project contains full samples. TestRpcCoroutineSupport
suspend fun findStrongestAttack(): StandProto.Attack {
val standService = StandServiceGrpc.newStub(managedChannel)
val characterService = CharacterServiceGrpc.newStub(managedChannel)
val deferredStands = characterService.getAllCharactersStream() //Service call returns a ReceiveChannel<Character>
.map { character ->
//Suspending unary call. Using the generated overloads we can rely on
//coroutines for deferred calls instead of listenable futures
async { standService.getStandByCharacter(character) }
}
.toList()
val strongestAttack = deferredStands
.flatMap { it.await().attacksList }
.maxBy { it.damage }
return strongestAttack ?: StandProto.Attack.getDefaultInstance()
}Bidirectional Rpc Channel Example
@Test fun `Test Bidirectional Rpc Channel`() = runBlocking {
val stub = StandServiceGrpc.newStub(grpcServerRule.channel)
//Bidi method overload returns a channel that accepts our request type (A Character) and
//returns our response type (A Stand)
val rpcChannel = stub.getStandsForCharacters()
//Our dummy service is sending three responses for each request it receives
rpcChannel.send(characters["Dio Brando"]!!)
stands["The World"].toString().let {
assertEquals(it,rpcChannel.receive().toString())
assertEquals(it,rpcChannel.receive().toString())
assertEquals(it,rpcChannel.receive().toString())
}
rpcChannel.send(characters["Jotaro Kujo"]!!)
stands["Star Platinum"].toString().let {
assertEquals(it,rpcChannel.receive().toString())
assertEquals(it,rpcChannel.receive().toString())
assertEquals(it,rpcChannel.receive().toString())
}
//Closing the channel has the same behavior as calling onComplete on the request stream observer.
//Calling close(throwable) behaves the same as onError(throwable)
rpcChannel.close()
//Assert that we consumed the expected number of responses from the stream
assertNull(rpcChannel.receiveOrNull(),"Response quantity was greater than expected")
}
Mock Service Generator
This generator creates mock implementations of proto service definitions. This is useful for orchestrating a set of expected responses, aiding in unit testing methods that rely on rpc calls.
Full example for mocking services in unit tests. The code generated relies on the kroto-plus-test artifact as a dependency. It is a small library that provides utility methods used by the mock services.
- If no responses are added to the response queue then the mock service will return the default instance of the response type.
- Currently only unary methods are being mocked, with support for other method types on the way
@Test fun `Test Unary Response Queue`(){
MockStandService.getStandByNameResponseQueue.apply {
//Queue up a valid response message
addMessage {
name = "Star Platinum"
powerLevel = 500
speed = 550
addAttacks(StandProtoBuilders.Attack {
name = "ORA ORA ORA"
damage = 100
range = StandProto.Attack.Range.CLOSE
})
}
//Queue up an error
addError(Status.INVALID_ARGUMENT)
}
val standStub = StandServiceGrpc.newBlockingStub(grpcServerRule.channel)
standStub.getStandByName { name = "Star Platinum" }.let{ response ->
assertEquals("Star Platinum",response.name)
assertEquals(500,response.powerLevel)
assertEquals(550,response.speed)
response.attacksList.first().let{ attack ->
assertEquals("ORA ORA ORA",attack.name)
assertEquals(100,attack.damage)
assertEquals(StandProto.Attack.Range.CLOSE,attack.range)
}
}
try{
standStub.getStandByName { name = "The World" }
fail("Exception was expected with status code: ${Status.INVALID_ARGUMENT.code}")
}catch (e: StatusRuntimeException){
assertEquals(Status.INVALID_ARGUMENT.code, e.status.code)
}
}Message Builder Lambda Generator
This generator creates lambda based builders for message types
val attack = StandProtoBuilders.Attack {
name = "ORA ORA ORA"
damage = 100
range = StandProto.Attack.Range.CLOSE
}
//Copy extensions are also generated
val newAttack = attack.copy { damage = 200 }
User Defined External Generators
This feature is currently in development. Api documentation and sample project are in the works.
Getting Started With Gradle
Using Plugin DSL
plugins{
id 'com.github.marcoferrer.kroto-plus' version '0.1.2'
}Using buildscript block (Legacy)
buildscript{
ext.krotoplusVersion = '0.1.2'
repositories {
jcenter()
}
dependencies{
classpath "com.github.marcoferrer.krotoplus:kroto-plus-gradle-plugin:${krotoplusVersion}"
}
}
apply plugin: 'com.github.marcoferrer.kroto-plus'Configuring Kroto+ Codegen
def generatedOutputDir = "$buildDir/generated-sources/main/kotlin"
sourceSets {
main {
kotlin{
srcDirs += generatedOutputDir
}
}
}
clean.doFirst{
delete generatedOutputDir
}
krotoPlus{
//Proto definition source directories, or path to a jar containing proto definitions
sources = [
"$projectDir/src/main/proto",
"$buildDir/extracted-include-protos/main"
]
//The default file output directory for all generators
defaultOutputDir = file(generatedOutputDir)
//Number of concurrent file writers (Default 3)
//More does not equal better here. Too many writers can lead to a decrease in performance
//and adjustments should be based on the overall quantity of proto files being processed.
fileWriterCount = 4
//Block used for enabling individual code generators and configuring their settings
generators{
stubOverloads{
//[Optional] Output directory specific to the files created by this generator
outputDir = file(generatedOutputDir)
//[Optional (Default: false)] Generate coroutine extensions for service stub rpc methods
supportCoroutines = true
}
mockServices{
//[Optional] Output directory specific to the files created by this generator
//Normally this should point to a test sources directory
outputDir = file(generatedOutputDir)
}
//Enabling a generator with no configurable or sufficient default options
protoTypeBuilders
/*
Enabling a custom external code generator
This feature is incubating and will be fully enabled in the near future
and include proper documentation
external('com.some.package.MyCustomGenerator'){
args = ['-foo','bar','-flag']
}
*/
}
}Road Map
- Document API for defining custom code generators
- Increase test coverage.
- Implement UP-TO-DATE checks in the gradle plugin
- Add Android compatibility to project
- Update gradle plugin to support Java 1.7 runtime
This project was made possible by the great work being done by the devs and contributors at Square and relies heavily on their open source projects Kotlin Poet and Wire

Formed in 2009, the Archive Team (not to be confused with the archive.org Archive-It Team) is a rogue archivist collective dedicated to saving copies of rapidly dying or deleted websites for the sake of history and digital heritage. The group is 100% composed of volunteers and interested parties, and has expanded into a large amount of related projects for saving online and digital history.
