Skip to content

Commit

Permalink
Add callbackSignature plugin option for generating unary callback s…
Browse files Browse the repository at this point in the history
…ignatures (#9)

Adding the `callbackSignature` option for call sites without kotlin
coroutines. This change basically exposes the wiring from the generated
code to the underlying unary request callback method.

| **Option** | **Type** | **Default** | **Repeatable** | **Details** |

|----------------------------|:--------:|:-----------:|:--------------:|-------------------------------------------------|
| `generateCallbackMethods` | Boolean | `false` | No | Generate callback
signatures for unary methods. |
| `generateCoroutineMethods` | Boolean | `true` | No | Generate suspend
signatures for unary methods. |


Interface example:
```kotlin
public interface ElizaServiceClientInterface {
    public suspend fun say(request: NoPackage.SayRequest, headers: Headers = emptyMap()):
        ResponseMessage<NoPackage.SayResponse>

    public fun say(
        request: NoPackage.SayRequest,
        headers: Headers = emptyMap(),
        onResult: (ResponseMessage<NoPackage.SayResponse>) -> Unit
    ): Cancelable
}
```
Implementation:
```kotlin
public class ElizaServiceClient(
    private val client: ProtocolClientInterface
) : ElizaServiceClientInterface {
    public override suspend fun say(request: NoPackage.SayRequest, headers: Headers):
        ResponseMessage<NoPackage.SayResponse> = client.unary(
        request,
        headers,
        MethodSpec(
            ".ElizaService/Say",
            NoPackage.SayRequest::class,
            NoPackage.SayResponse::class
        )
    )

    public override fun say(
        request: NoPackage.SayRequest,
        headers: Headers,
        onResult: (ResponseMessage<NoPackage.SayResponse>) -> Unit
    ): Cancelable = client.unary(
        request,
        headers,
        MethodSpec(
            ".ElizaService/Say",
            NoPackage.SayRequest::class,
            NoPackage.SayResponse::class
        ),
        onResult
    )
}
```
  • Loading branch information
buildbreaker authored Feb 27, 2023
1 parent 038afeb commit 1e28ec1
Show file tree
Hide file tree
Showing 13 changed files with 434 additions and 52 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ Comprehensive documentation for everything, including
[interceptors][interceptors], [streaming][streaming], and [error handling][error-handling]
is available on the [connect.build website][getting-started].

## Generation Options

| **Option** | **Type** | **Default** | **Repeatable** | **Details** |
|----------------------------|:--------:|:-----------:|:--------------:|-------------------------------------------------|
| `generateCallbackMethods` | Boolean | `false` | No | Generate callback signatures for unary methods. |
| `generateCoroutineMethods` | Boolean | `true` | No | Generate suspend signatures for unary methods. |

## Example Apps

Example apps are available in [`/examples`](./examples). First, run `make generate` to generate
Expand Down
3 changes: 3 additions & 0 deletions crosstests/buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ plugins:
- name: connect-kotlin
out: google-java/src/main/kotlin/generated
path: ./protoc-gen-connect-kotlin/protoc-gen-connect-kotlin
opt:
- generateCallbackMethods=true
- generateCoroutineMethods=true
- name: java
out: google-java/src/main/java/generated
- name: kotlin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ interface TestSuite {
suspend fun failUnary()
suspend fun failServerStreaming()
}

interface UnaryCallbackTestSuite {
suspend fun test(tag: String)
suspend fun emptyUnary()
suspend fun largeUnary()
suspend fun customMetadata()
suspend fun statusCodeAndMessage()
suspend fun specialStatus()
suspend fun unimplementedMethod()
suspend fun unimplementedService()
suspend fun failUnary()
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ class Main {
compressionPools = listOf(GzipCompressionPool)
)
)
tests(tag, connectClient, shortTimeoutClient)
coroutineTests(tag, connectClient, shortTimeoutClient)
callbackTests(tag, connectClient)
}

private suspend fun tests(
private suspend fun coroutineTests(
tag: String,
protocolClient: ProtocolClient,
shortTimeoutClient: ProtocolClient
Expand All @@ -114,5 +115,22 @@ class Main {

testServiceClientSuite.test(tag)
}

private suspend fun callbackTests(
tag: String,
protocolClient: ProtocolClient
) {
val testServiceClientSuite = TestServiceClientCallbackSuite(protocolClient)
testServiceClientSuite.emptyUnary()
testServiceClientSuite.largeUnary()
testServiceClientSuite.customMetadata()
testServiceClientSuite.statusCodeAndMessage()
testServiceClientSuite.specialStatus()
testServiceClientSuite.unimplementedMethod()
testServiceClientSuite.unimplementedService()
testServiceClientSuite.failUnary()

testServiceClientSuite.test(tag)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright 2022-2023 Buf Technologies, Inc.
//
// 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
//
// http://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.

package build.buf.connect.crosstest

import build.buf.connect.Code
import build.buf.connect.crosstest.ssl.UnaryCallbackTestSuite
import build.buf.connect.impl.ProtocolClient
import com.google.protobuf.ByteString
import com.grpc.testing.ErrorDetail
import com.grpc.testing.TestServiceClient
import com.grpc.testing.UnimplementedServiceClient
import com.grpc.testing.echoStatus
import com.grpc.testing.empty
import com.grpc.testing.errorDetail
import com.grpc.testing.payload
import com.grpc.testing.simpleRequest
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis

class TestServiceClientCallbackSuite(
client: ProtocolClient
) : UnaryCallbackTestSuite {
private val testServiceConnectClient = TestServiceClient(client)
private val unimplementedServiceClient = UnimplementedServiceClient(client)

private val tests = mutableListOf<Pair<String, suspend () -> Unit>>()

override suspend fun test(tag: String) {
println()
tests.forEachIndexed { index, (testName, test) ->
print("[$tag] Executing test case ${index + 1}/${tests.size}: $testName")
val millis = measureTimeMillis {
test()
}
println(" [$millis ms]")
}
}

private fun register(testName: String, test: suspend () -> Unit) {
tests.add(testName to test)
}

override suspend fun emptyUnary() = register("empty_unary") {
val countDownLatch = CountDownLatch(1)
testServiceConnectClient.emptyCall(empty {}) { response ->
response.failure {
fail<Unit>("expected error to be null")
}
response.success { success ->
assertThat(success.message).isEqualTo(empty {})
countDownLatch.countDown()
}
}
countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}

override suspend fun largeUnary() = register("large_unary") {
val size = 314159
val message = simpleRequest {
responseSize = size
payload = payload {
body = ByteString.copyFrom(ByteArray(size))
}
}
val countDownLatch = CountDownLatch(1)
testServiceConnectClient.unaryCall(message) { response ->
response.failure {
fail<Unit>("expected error to be null")
}
response.success { success ->
assertThat(success.message.payload?.body?.toByteArray()?.size).isEqualTo(size)
countDownLatch.countDown()
}
}
countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}

override suspend fun customMetadata() = register("custom_metadata") {
val size = 314159
val leadingKey = "x-grpc-test-echo-initial"
val leadingValue = "test_initial_metadata_value"
val trailingKey = "x-grpc-test-echo-trailing-bin"
val trailingValue = byteArrayOf(0xab.toByte(), 0xab.toByte(), 0xab.toByte())
val headers =
mapOf(
leadingKey to listOf(leadingValue),
trailingKey to listOf(trailingValue.b64Encode())
)
val message = simpleRequest {
responseSize = size
payload = payload { body = ByteString.copyFrom(ByteArray(size)) }
}
val countDownLatch = CountDownLatch(1)
testServiceConnectClient.unaryCall(message, headers) { response ->
assertThat(response.code).isEqualTo(Code.OK)
assertThat(response.headers[leadingKey]).containsExactly(leadingValue)
assertThat(response.trailers[trailingKey]).containsExactly(trailingValue.b64Encode())
response.failure {
fail<Unit>("expected error to be null")
}
response.success { success ->
assertThat(success.message.payload!!.body!!.size()).isEqualTo(size)
countDownLatch.countDown()
}
}
countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}

override suspend fun statusCodeAndMessage() = register("status_code_and_message") {
val message = simpleRequest {
responseStatus = echoStatus {
code = Code.UNKNOWN.value
message = "test status message"
}
}
val countDownLatch = CountDownLatch(1)
testServiceConnectClient.unaryCall(message) { response ->
assertThat(response.code).isEqualTo(Code.UNKNOWN)
response.failure { errorResponse ->
assertThat(errorResponse.error).isNotNull()
assertThat(errorResponse.code).isEqualTo(Code.UNKNOWN)
assertThat(errorResponse.error.message).isEqualTo("test status message")
countDownLatch.countDown()
}
response.success {
fail<Unit>("unexpected success")
}
}

countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}

override suspend fun specialStatus() = register("special_status") {
val statusMessage =
"\\t\\ntest with whitespace\\r\\nand Unicode BMP ☺ and non-BMP \uD83D\uDE08\\t\\n"
val countDownLatch = CountDownLatch(1)
testServiceConnectClient.unaryCall(
simpleRequest {
responseStatus = echoStatus {
code = 2
message = statusMessage
}
}
) { response ->
response.failure { errorResponse ->
val error = errorResponse.error
assertThat(error.code).isEqualTo(Code.UNKNOWN)
assertThat(response.code).isEqualTo(Code.UNKNOWN)
assertThat(error.message).isEqualTo(statusMessage)
countDownLatch.countDown()
}
response.success {
fail<Unit>("unexpected success")
}
}
countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}

override suspend fun unimplementedMethod() = register("unimplemented_method") {
val countDownLatch = CountDownLatch(1)
testServiceConnectClient.unimplementedCall(empty {}) { response ->
assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED)
countDownLatch.countDown()
}
countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}

override suspend fun unimplementedService() = register("unimplemented_service") {
val countDownLatch = CountDownLatch(1)
unimplementedServiceClient.unimplementedCall(empty {}) { response ->
assertThat(response.code).isEqualTo(Code.UNIMPLEMENTED)
countDownLatch.countDown()
}
countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}

override suspend fun failUnary() = register("fail_unary") {
val expectedErrorDetail = errorDetail {
reason = "soirée 🎉"
domain = "connect-crosstest"
}
val countDownLatch = CountDownLatch(1)
testServiceConnectClient.failUnaryCall(simpleRequest {}) { response ->
assertThat(response.code).isEqualTo(Code.RESOURCE_EXHAUSTED)
response.failure { errorResponse ->
val error = errorResponse.error
assertThat(error.code).isEqualTo(Code.RESOURCE_EXHAUSTED)
assertThat(error.message).isEqualTo("soirée 🎉")
val connectErrorDetails = error.unpackedDetails(ErrorDetail::class)
assertThat(connectErrorDetails).containsExactly(expectedErrorDetail)
countDownLatch.countDown()
}
response.success {
fail<Unit>("unexpected success")
}
}
countDownLatch.await(500, TimeUnit.MILLISECONDS)
assertThat(countDownLatch.count).isZero()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,6 @@ class TestServiceClientSuite(
}
}

private fun ByteArray.b64Encode(): String {
internal fun ByteArray.b64Encode(): String {
return this.toByteString().base64()
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ class Main {
compressionPools = listOf(GzipCompressionPool)
)
)
tests(tag, connectClient, shortTimeoutClient)
suspendTests(tag, connectClient, shortTimeoutClient)
}

private suspend fun tests(
private suspend fun suspendTests(
tag: String,
protocolClient: ProtocolClient,
shortTimeoutClient: ProtocolClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,6 @@ class TestServiceClientSuite(
}
}

private fun ByteArray.b64Encode(): String {
internal fun ByteArray.b64Encode(): String {
return this.toByteString().base64()
}
4 changes: 4 additions & 0 deletions protoc-gen-connect-kotlin/buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ plugins:
- name: connect-kotlin
out: src/test/java/
path: ./protoc-gen-connect-kotlin/protoc-gen-connect-kotlin
opt:
- generateCallbackMethods=true
- generateCoroutineMethods=true
- name: java
out: src/test/java/

Loading

0 comments on commit 1e28ec1

Please sign in to comment.