Generating a Server

guardrail-generated servers come in two parts: a Resource and a Handler. The Resource contains all the routing logic, accepting a Handler as an argument to the route function in order to provide an HTTP service in whichever supported HTTP framework you’re hosting your service in.

The following is an example from the akka-http server generator:


// The `Handler` trait is fully abstracted from the underlying http framework. As a result, with the exception of some
// structural alterations (`F[_]` instead of `Future[_]` as the return type) the same handlers can be used with
// different `Resource` implementations from different framework generators. This permits greater compatibility between
// different frameworks without changing your business logic.
  
trait UserHandler {
  def createUser(respond: UserResource.CreateUserResponse.type)(body: definitions.User): scala.concurrent.Future[UserResource.CreateUserResponse]
  def createUsersWithArrayInput(respond: UserResource.CreateUsersWithArrayInputResponse.type)(body: Vector[definitions.User]): scala.concurrent.Future[UserResource.CreateUsersWithArrayInputResponse]
  def createUsersWithListInput(respond: UserResource.CreateUsersWithListInputResponse.type)(body: Vector[definitions.User]): scala.concurrent.Future[UserResource.CreateUsersWithListInputResponse]
  def loginUser(respond: UserResource.LoginUserResponse.type)(username: String, password: String): scala.concurrent.Future[UserResource.LoginUserResponse]
  def logoutUser(respond: UserResource.LogoutUserResponse.type)(): scala.concurrent.Future[UserResource.LogoutUserResponse]
  def getUserByName(respond: UserResource.GetUserByNameResponse.type)(username: String): scala.concurrent.Future[UserResource.GetUserByNameResponse]
  def updateUser(respond: UserResource.UpdateUserResponse.type)(username: String, body: definitions.User): scala.concurrent.Future[UserResource.UpdateUserResponse]
  def deleteUser(respond: UserResource.DeleteUserResponse.type)(username: String): scala.concurrent.Future[UserResource.DeleteUserResponse]
}

object UserResource {
  def routes(handler: UserHandler)(implicit mat: akka.stream.Materializer): Route = {
    {
      path("v2" / "user")(post(entity(as[definitions.User](createUserDecoder)).apply(body => complete(handler.createUser(CreateUserResponse)(body)))))
    } ~ ({
      path("v2" / "user" / "createWithArray")(post(entity(as[Vector[definitions.User]](createUsersWithArrayInputDecoder)).apply(body => complete(handler.createUsersWithArrayInput(CreateUsersWithArrayInputResponse)(body)))))
    }) ~ ({
      path("v2" / "user" / "createWithList")(post(entity(as[Vector[definitions.User]](createUsersWithListInputDecoder)).apply(body => complete(handler.createUsersWithListInput(CreateUsersWithListInputResponse)(body)))))
    }) ~ ({
      path("v2" / "user" / "login")(get((parameter(Symbol("username").as[String](stringyJsonUnmarshaller.andThen(unmarshallJson[String]))) & parameter(Symbol("password").as[String](stringyJsonUnmarshaller.andThen(unmarshallJson[String])))).apply((username, password) => discardEntity(complete(handler.loginUser(LoginUserResponse)(username, password))))))
    }) ~ ({
      path("v2" / "user" / "logout")(get(discardEntity(complete(handler.logoutUser(LogoutUserResponse)()))))
    }) ~ ({
      path("v2" / "user" / Segment).apply(username => get(discardEntity(complete(handler.getUserByName(GetUserByNameResponse)(username)))))
    }) ~ ({
      path("v2" / "user" / Segment).apply(username => put(entity(as[definitions.User](updateUserDecoder)).apply(body => complete(handler.updateUser(UpdateUserResponse)(username, body)))))
    }) ~ ({
      path("v2" / "user" / Segment).apply(username => delete(discardEntity(complete(handler.deleteUser(DeleteUserResponse)(username)))))
    })
  }
  `...`
}

As all parameters are provided as arguments to the function stubs in the trait, there’s no concern of forgetting to extract a query string parameter, introducing a typo in a form parameter name, or forgetting to close the bytestream for the streaming HTTP Request.

Separation of business logic

Providing an implementating of a function with a well-defined set of inputs and outputs is natural for any developer. By reducing the scope of the interface a developer writes against, implementations are more clear and concise.

Furthermore, by providing business logic as an implementation of an abstract class, unit tests can test the routing layer and business logic independently, by design.

API structure slip is impossible

As parameters are explicitly provided as arguments to functions in Handlers, any alteration to parameters constitute a new function interface that must be implemented. As a result, if providing an implementation for an externally managed specification, the compiler informs when a previously written function is no longer sufficient.

By representing different response codes and structures as members of a sealed trait, it’s impossible to return a structure that violates the specification, even for less frequently used response codes.

Finally, describing an endpoint in your specification without providing an implementation for it is a compiler error. This prevents reduction of functionality due to refactors, human error, or miscommunication with other teams.

Extracting custom data from a request

In some cases, you may wish to extract data from a request and inject it into your handler, without specifying the extracted data in the OpenAPI definition. Common use cases include integrating with existing Directives, accessing underlying data provided by akka-http but without a direct analog in OpenAPI, as well as providing an escape hatch to inject functionality expressed via akka-http’s Directive directly into the Akka HTTP routes generated by guardrail.

If using the guardrail CLI, supply --custom-extraction when generating your server in order to get this functionality.

Once the feature is enabled, the generated code from the above example is modified to look like this:

trait UserHandler[-E] {
  def createUser(respond: UserResource.CreateUserResponse.type)(body: definitions.User)(extracted: E): scala.concurrent.Future[UserResource.CreateUserResponse]
  // ...
}

object UserResource {
  def routes[E](handler: UserHandler, customExtract: String => Directive1[E])(implicit mat: akka.stream.Materializer): Route = {
    // ...
  }
}

You may now provide a function that accepts the operationId of the route, and returns a Directive1[E], where, E is an arbitrary type that will be passed through to your handlers. The Directive1[E] will be injected into the generated routes after the path and method Directives.

Note: If you have any Directive with an arity different to Directive1 (for instance, Directive0 or Directive5) you must convert it into a Directive1 via myCoolDirective.tmap(Tuple1(_)) or similar. This is done to provide a consistent user experience, without the added complexity of the so-called “magnet pattern”.

For example, to extract an X-User-Id header value from an incoming request, your code might look like this:

class UserApi extends UserHandler[String] {
  override def createUser(respond: UserResource.CreateUserResponse.type)(body: definitions.User)(userIdHeader: String): scala.concurrent.Future[UserResource.CreateUserResponse] = {
    println(s"The supplied X-User-Id header is: $userIdHeader")
    ???
  }
}

val extractXUserId = (operationId: String) => headerValueByName("X-User-Id") // Directive from Akka HTTP

val userRoutes = UserResource.routes(new UserApi, extractXUserId)

Because E is an arbitrary type, you may extract anything, including the full HttpRequest itself. Multiple values may be extracted using tuples. If you do not wish to extract anything, perhaps because the Directive acts as a gate which passes some requests and rejects others, simply provide String => Directive1[Unit] and write your handler implementation to extend Handler[Unit].

Generating test-only (real) server mocks for unit tests

Often, we’ll also want to have mock HTTP clients for use in unit tests. Mocking requires stringent adherence to the specification, otherwise our mock clients are unrepresentative of the production systems they are intending to mock. The following is an example of a “mock” HTTP Client generated by guardrail; it speaks real HTTP, though doesn’t need to bind to a port in order to run. This permits parallelized tests to be run without concern of port contention.

val userRoutes: Route = UserResource.routes(new UserHandler {
  override def getUserByName(respond: UserResource.getUserByNameResponse.type)(username: String): scala.concurrent.Future[UserResource.getUserByNameResponse] = {
    if (username == "foo") {
      Future.successful(respond.OK(User(id=Some(1234L), username=Some("foo"))))
    } else {
      Future.successful(respond.NotFound)
    }
  }
})
val userHttpClient: HttpRequest => Future[HttpResponse] = Route.asyncHandler(userRoutes)
val userClient: UserClient = UserClient.httpCLient(userHttpClient)
val getUserResponse: EitherT[Future, Either[Throwable, HttpResponse], User] = userClient.getUserByName("foo").map(_.fold(user => user))
val user: User = getUserResponse.value.futureValue.right.value // Unwraps `User(id=Some(1234L), username=Some("foo"))` using scalatest's `ScalaFutures` and `EitherValues` unwrappers.

This strategy of mocking ensures we follow the spec, even when the specification changes. This means not only more robust tests, but also tests that communicate failures via compiler errors instead of at runtime. Having a clear separation of where errors can come from permits trusting our tests more. If the tests compile, any and all errors that occur are in the domain of business logic.

One other strategy for testing non-guardrail generated clients is to bind userRoutes from above to a port, run tests that use hand-rolled or vendor-supplied HTTP clients, then unbind the port when the test ends:

val binding: ServerBinding =
  Http().bindAndHandle(userRoutes, "localhost", 1234).futureValue

// run tests

binding.unbind().futureValue

A note about scalatest integration

akka-http

The default ExceptionHandler in akka-http swallows exceptions, so if you intend to fail() tests from inside guardrail-generated HTTP Servers, you’ll likely want to have the following implicit in scope:

implicit def exceptionHandler: ExceptionHandler = new ExceptionHandler {
  def withFallback(that: ExceptionHandler): ExceptionHandler = this
  def seal(settings: RoutingSettings): ExceptionHandler = this

  def isDefinedAt(error: Throwable) = error.isInstanceOf[org.scalatest.TestFailedException]
  def apply(error: Throwable) = throw error
}

This passes all TestFailedExceptions through to the underlying infrastructure. In our tests, when we call:

val userClient: UserClient = UserClient.httpCLient(userHttpClient)
val getUserResponse: EitherT[Future, Either[Throwable, HttpResponse], User] = userClient.getUserByName("foo")
val user: User = getUserResponse.map(_.fold(user => user)).value.futureValue.right.value

futureValue will raise the TestFailedException with the relevant stack trace.

Prev: Sample API specification Next: Generating clients