Actions are Misk's unit for an endpoint. Misk lets you make HTTP actions, and gRPC actions via Wire.
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)
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)
}
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()
}
}
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
gives an action access to context produced by the action's interceptors.
Misk has a few ActionScoped
items built in:
MiskCaller
- access derived authorization detailsHttpCall
- access lower level HTTP details, e.g. request headers
Use tests annotated with @MiskTest
to perform tests. There are two common patterns
to testing actions:
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...
}
Use @WithMiskCaller
for ActionScoped<MiskCaller>
:
@MiskTest
@WithMiskCaller(user = "test-user") // or @WithMiskCaller(service = "test-service")
class MyTest {
@MiskTestModule val module = MyModule()
@Inject lateinit var action: HelloWebAction // or any other class that injects ActionScoped<MiskCaller>
// use action...
}
For types other than MiskCaller
, use ActionScope
directly either within your setup and teardown test methods:
@MiskTest
class MyTest {
@MiskTestModule val module = MyModule()
@Inject lateinit var actionScope: ActionScope
@Inject lateinit var action: HelloWebAction // or any other class that injects ActionScoped<MyScopedObject>
@BeforeEach fun setUp() {
actionScope.create(
mapOf(
keyOf<MyScopedObject>() to MyScopedObject()
)
).enter()
}
@AfterEach fun tearDown() {
actionScope.close()
}
@Test fun test() {
// use action...
}
}
...or within the test itself:
@MiskTest
class MyTest {
@MiskTestModule val module = MyModule()
@Inject lateinit var actionScope: ActionScope
@Inject lateinit var action: HelloWebAction
@Test fun test() {
actionScope.create(
mapOf(
keyOf<MyScopedObject>() to MyScopedObject()
)
).inScope {
// use action or class which injects ActionScoped<MyScopedObject>...
}
}
}
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()
}
}