Skip to content

Actions

Actions are Misk’s unit for an endpoint. Misk lets you make HTTP actions, and gRPC actions via Wire.

Web Actions

Below are some example Web action declarations. Note that many of the annotations are optional.

Calls are authenticated at the service-level (service is listed in the @Authenticated annotation) or at the user-level (user has at least one of the capabilities listed in the @Authenticated annotation).

GET:

@Singleton
class HelloWebAction @Inject constructor() : WebAction {
  @Get("/hello/{name}") // Enclose path parameters in {}
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  @Authenticated(services = ["my-other-app"], capabilities = ["my-app_owners"])
  fun hello(
    // Use @PathParam with the name of the param. Required if there's a param in the path pattern.
    @PathParam name: String,
    // RequestHeaders is optional:
    @RequestHeaders headers: Headers,
    // QueryParams are optional:
    @QueryParam nickName: String?, // e.g. /hello/abc?nickName=def
    @QueryParam greetings: List<String>? // e.g. /hello/abc?greetings=def&greetings=ghi
  ): HelloResponse {
    return HelloResponse(name)
  }
}

POST:

@Singleton
class HelloWebPostAction @Inject constructor() : WebAction {
  @Post("/hello/{name}")
  @RequestContentType(MediaTypes.APPLICATION_JSON)
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  @Authenticated(services = ["my-other-app"], capabilities = ["my-app_owners"])
  fun hello(
      @PathParam name: String, 
      // RequestBody is optional, and is automatically deserialized to the provided type.
      @RequestBody body: PostBody
  ): HelloPostResponse {
    return HelloPostResponse(body.greeting, name)
  }
}

data class HelloPostResponse(val greeting: String, val name: String)

data class PostBody(val greeting: String)

Setting up HTTP actions

Install the action into a module:

class HelloModule : KAbstractModule() {
  override fun configure() {
    install(WebActionModule.create<HelloWebAction>())
    install(WebActionModule.create<HelloWebPostAction>())
  }
}

And then put that module onto the top level MiskApplication.

fun main(args: Array<String>) {
  MiskApplication(
    // ...
    HelloModule(), // new!
  ).run(args)
}

Response type

If you change the action’s response type to Response<T>, it gives you better control over the response status code and headers.

@Singleton
class HelloWebResponseAction @Inject constructor() : WebAction {
  @Get("/hello_but_203/{name}")
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  fun hello(@PathParam name: String): Response<HelloResponse> = Response(
    statusCode = 203,
    headers = headersOf(),
    body = HelloResponse()
  )
}

It’s also possible to throw exceptions that are mapped to status codes.

@Singleton
class HelloWebResponseAction @Inject constructor() : WebAction {
  @Get("/no_access/{name}")
  fun hello(@PathParam name: String): HelloResponse {
      throw UnauthenticatedException()
  }
}

gRPC Actions

Misk has support for gRPC actions via the Wire protocol buffer (protobuf) library.

To create a gRPC action, first define the relevant protos for your service. Let’s say we’re creating a GreeterService that exposes one API, Hello. Create this file in src/main/proto/hello.proto:

syntax = "proto2";
package squareup.cash.hello;

option java_package = "com.squareup.protos.cash.hello";

message HelloRequest {
  optional string message = 1;
}

message HelloResponse {
  optional string message = 1;
}

service GreeterService {
  rpc Hello(HelloRequest) returns (HelloResponse) {}
}

Next, in your project’s build file (for this example, build.gradle.kts), add a dependency on the wire plugin:

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

Add the following configuration to generate the gRPC interfaces for your service:

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

  kotlin {
    include("squareup.cash.hello.GreeterService")
    rpcCallStyle = "blocking"
    rpcRole = "server"
    singleMethodServices = true
  }

  java {
  }
}

Finally, implement and bind your gRPC action. GreeterServiceHelloBlockingServer is generated by Wire.

@Singleton
class HelloGrpcAction @Inject internal constructor()
  : GreeterServiceHelloBlockingServer, WebAction {

  @Unauthorized
  override fun Hello(request: HelloRequest): HelloResponse {
    return HelloResponse("message")
  }
}

// This module binds HelloGrpcAction.
class GreeterActionModule : KAbstractModule() {
  override fun configure() {
    install(WebActionModule.create<HelloGrpcAction>())
  }
}

Creating a gRPC action automatically creates a JSON endpoint with all of the same annotations in the path defined by the ...BlockingServer, typically /<package>.<service name>/<rpc name>.

You can also create a second class that extends WebAction to customize this further. Read more about HTTP actions in Web Actions. If you’re building both a gRPC and a HTTP action, a common pattern is to have them both use a common dependency:

@Singleton
class HelloGrpcAction @Inject constructor(val greeter: Greeter)
  : GreeterServiceHelloBlockingServer, WebAction {
  @Unauthorized override fun hello() = HelloResponse(greeter.greet())
}

@Singleton
class HelloWebAction @Inject constructor(val greeter: Greeter) : WebAction {
  @Unauthorized
  @Get("/hello")
  @ResponseContentType(MediaTypes.APPLICATION_JSON)
  fun hello() = HelloResponse(greeter.greet())
}

@Singleton
class Greeter @Inject constructor()  {
  fun greet() = "Hello world"
}

ActionScoped

ActionScoped gives an action access to context produced by the action’s interceptors.

Misk has a few ActionScoped items built in:

  • MiskCaller - access derived authorization details
  • HttpCall - access lower level HTTP details, e.g. request headers

Testing

Use tests annotated with @MiskTest to perform tests. There are two common patterns to testing actions:

Test Action classes directly

Make sure that the module under test contains a Guice binding for the action and its dependencies, then inject your action.

class MyModule : KAbstractModule() {
  override fun configure() {
    install(WebActionModule.create<HelloWebAction>())
    // Alternatively, a direct or just-in-time binding might be sufficient.
  }
}

@MiskTest class MyTest {
  @MiskTestModule val module = MyModule()
  @Inject lateinit var action: HelloWebAction

  // use action...
}

Testing ActionScoped

Use @WithMiskCaller for ActionScoped<MiskCaller>:

@MiskTest
@WithMiskCaller
class MyTest {
  @MiskTestModule val module = MyModule()
  @Inject lateinit var action: HelloWebAction
  @Inject lateinit var miskCaller: ActionScoped<MiskCaller>

  // use action...
}

Otherwise, use ActionScope directly:

@MiskTest
class MyTest {
  @MiskTestModule val module = MyModule()
  @Inject lateinit var actionScope: ActionScope
  @Inject lateinit var action: HelloWebAction
  @Inject lateinit var myScopedObject: ActionScoped<MyScopedObject>

  @BeforeEach fun setUp() {
    actionScope.enter(
      mapOf(
        keyOf<MyScopedObject>() to MyScopedObject()
      )
    )
  }

  @AfterEach fun tearDown() {
    actionScope.provider.get().close()
  }

  // use action...
}

Integration tests

It’s possible to perform tests terminating at the app’s HTTP/gRPC interface.

The module under test should include WebServerTestingModule so that Misk stands up a server during tests:

class MyModule : KAbstractModule() {
   override fun configure(){
    install(WebServerTestingModule())
    // install other modules...
 }
}

Then test HTTP requests using WebTestClient (assertions here were made using Kotest):

@MiskTest(startService = true)
class HelloWebIntegrationTest {
  @Suppress("unused")
  @MiskTestModule val module = MyModule()

  @Inject lateinit var webTestClient: WebTestClient

  @Test
  fun `makes a call to the service`() {
    val response = webTestClient.post("/hello", HelloRequest("world"))
    response.response.code shouldBe 200
    response.parseJson<HelloResponse>() shouldBe HelloResponse("hello world")
  }
}

Test gRPC requests by setting up a gRPC client pointing to the running app:

class MyServerModule : KAbstractModule() {
  override fun configure() {
    install(WebServerTestingModule())
    // Assume RobotLocatorWebAction is a gRPC action.
    install(WebActionModule.create<RobotLocatorWebAction>())
  }
}

class MyClientModule(val jetty: JettyService): KAbstractModule() {
  override fun configure() {
    // Assume RobotLocator was generated via Wire.
    install(GrpcClientModule.create<RobotLocator, GrpcRobotLocator>("robots"))
  }

  @Provides
  @Singleton
  fun provideHttpClientConfig(): HttpClientsConfig {
    return HttpClientsConfig(
      endpoints = mapOf(
        "robots" to HttpClientEndpointConfig(jetty.httpServerUrl.toString())
      )
    )
  }
}

@MiskTest(startService = true)
class RobotLocatorIntegrationTest {
  @Suppress("unused")
  @MiskTestModule val module = MyServerModule()

  @Inject lateinit var jettyService: JettyService

  @Test 
  fun `makes a call to the service`() {
    val robotLocator = Guice.createInjector(MyClientModule(jettyService))
      .getInstance<RobotLocator>()

    robotLocator.SayHello().executeBlocking(HelloRequest.builder().name("world").build()) shouldBe 
      HelloReply.Builder().message("hello world").build()
  }
}