Clients

May 4, 2021 ยท View on GitHub

Misk provides configurable HTTP clients built on top of OkHttp and Retrofit, and gRPC clients built on top of Wire.

HTTP Clients

Config

Set up a Config object (and use MiskConfig.load to load config from a YAML file):

data class MyServiceConfig(
  val http_clients: HttpClientsConfig,
  // ...
) : Config

In the configuration YAML, specify the target service's address:

http_clients:
  ...
  endpoints:
    ...
    greeter:  { url: "https://hello.example.com" }

Create an OkHttpClient

Use HttpClientModule:

class MyClientModule : KAbstractModule() {
  override fun configure() {
    install(HttpClientModule(
      // Corresponds to the YAML config. Requires a bound HttpClientsConfig
      name = "greeter",
      // Optional annotation to define how you inject your client dependency
      annotation = Names.named("greeterHttp")
    ))
  }
}

This binds an OkHttpClient that you can inject:

class MyClient @Inject constructor(@Named("greeterHttp") val client: OkHttpClient) {
  fun callGreeter() {
    val response = client.newCall(
      Request.Builder()
        .url("http://localhost:8080/hello")
        .build()
    ).execute()
  }
}

Create typed clients with Retrofit

First, create a Retrofit interface. See the Retrofit docs for more details.

interface GreeterApi {
  @POST("/hello")
  @Headers(value = ["accept: application/json"])
  fun hello(
    @Body request: HelloRequest
  ): Call<HelloResponse>
}

Next, install a TypedHttpClientModule with this interface.

class HelloClientModule : KAbstractModule() {
  override fun configure() {
    install(
      TypedHttpClientModule(
        GreeterApi::class, 
        // Corresponds to the YAML config. Requires a bound HttpClientsConfig
        name = "greeter", 
        // Optional annotation to define how you inject your client dependency
        annotation = Names.named("greeterApi")
      )
    )
  }
}

Now you can inject an implementation of this client:

@Singleton class MyApiClient @Inject constructor(
  @Named("greeterApi") private val api: GreeterApi
) {
  fun hello(message: String): String {
    val response = api.hello(
      HelloRequest(
        message = message
      )
    ).execute()
  }
}

gRPC Clients

First, include the auto-generated gRPC client code at the caller module using the Wire Gradle plugin.

plugins {
  id("com.squareup.wire")
}

wire {
  sourcePath {
    srcDir("src/main/proto")
  }

  // Generate Kotlin for the gRPC client API.
  kotlin {
    // Set this to false if you're generating client and server interfaces in one module
    exclusive = false
    includes ("squareup.cash.hello.GreeterService")
    rpcRole = "client"
  }

  java {
  }
}

Next, bind your client in code in a similar fashion to an HTTP client. Set up client configuration, as described in Config. Then, bind a GrpcClientModule:

class GreeterClientModule : KAbstractModule() {
  override fun configure() {
    install(GrpcClientModule.create<GreeterServiceClient, GrpcGreeterServiceClient>(
        // Corresponds to the YAML config. Requires a bound HttpClientsConfig
        name = "greeter",
        // Optional annotation to define how you inject your client dependency
        annotation = Names.named("greeterGrpc")
    ))
  }
}

With this all setup, you can now inject your client in source code and connect via gRPC:

internal class GrpcGreeterServiceClient @Inject internal constructor(
  @Named("greeterGrpc") private val greeterGrpc: GreeterServiceClient
) {
  fun get(message: String) {
    val response = greeterGrpc.Hello().executeBlocking(HelloRequest(message))

    // ... do something with the response here
  }
}