Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Swagger for Akka HTTP

31.3.2016 | 10 minutes of reading time

In this post I’m hoping to give you a good insight in how to setup Swagger for Akka HTTP.

Swagger is a tool to document REST API’s and overall works quite nicely to test some basic API commands. It already has integrations with many languages / REST frameworks. Since recently, Akka HTTP has been fully incorporated in the Akka platform. Swagger support for Akka HTTP exists, but the setup has some oddities. Here, I hope to provide you with a complete guideline.

Swagger Background

Swagger consists of two main parts; the Swagger UI and a server-side library. The server-side library is responsible for generating the API documentation from the code and supplemental annotations. This generated file is parsed by the Swagger UI, which in turn will provide an HTML app to view and test your API.

The generated API documentation looks something like this (in JSON):


{
  "tags": [{
    "name": "users"
  }],
  "host": "0.0.0.0:8080",
  "paths": {
    "/users": {
      "get": {
        "description": "",
        "tags": ["users"],
        "operationId": "getAllUsers",
        "produces": ["application/json"],
        "parameters": [],
        "summary": "Get list of all users",
        "responses": {
          "200": {
            "description": "successful operation",
            "schema": {
              "type": "array",
              "uniqueItems": true,
              "items": {
                "$ref": "#/definitions/User"
              }
            }
          }
        }
      }
    }
  },
  "basePath": "/",
  "info": {
    "description": "",
    "version": "1.0",
    "title": "",
    "termsOfService": ""
  },
  "schemes": ["http"],
  "definitions": {
    "Function1RequestContextFutureRouteResult": {
      "type": "object"
    },
    "User": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      }
    }
  },
  "swagger": "2.0"
}

As server-side library for generating the API documentation, we will use the swagger-akka-http project.

Project Setup and Dependencies

A new project may be generated with sbt-fresh , giving us already a nice project setup containing most of what we need. The layout should look like this;


+ build.sbt             // specific settings for (single) module
+ project
--+ build.properties    // sbt version
--+ Build.scala         // common settings for all modules
--+ Dependencies.scala  // values for library dependencies
--+ plugins.sbt         // sbt-git, sbt-header, sbt-scalariform
+ README.md
+ src
--+ main
----+ resources
----+ scala
------+ package.scala   // type aliases repoining `Seq` and friends to immutable

For the swagger-akka-http library (and for Akka itself), we need to add the following dependencies to the build.sbt file. Be nice and add these dependencies as variables to the project/Dependencies.scala file 😉

1"com.typesafe.akka" %% "akka-http-experimental" % 2.4.2
2  "de.heikoseeberger" %% "akka-http-circe" % 1.5.2
3  "io.circe" %% "circe-generic" % 0.3.0
4  "io.circe" %% "circe-java8" % 0.3.0
5  "com.github.swagger-akka-http" %% "swagger-akka-http" % 0.6.2

We would like to include the Swagger UI in our app – especially in this demo – because it lets us easily serve the necessary pages. For this, download the entire latest Swagger UI repository as ZIP from GitHub . Extract the archive and copy all files under the dist folder to our project’s src/main/resources/swagger/ folder.

Now we’re all set for some real code.

Akka HTTP Setup

For our demo application, the API itself is not really that interesting. Let’s assume we want to retrieve the users in our system, and add new users. This at least gives us a HTTP GET and POST request to describe and play with.

First we need an entry point for our application; the main class so to say. It will start our Actor system and from there run the HTTP service as Actor. The full code is here for UserApp.scala:

1import akka.actor._
2 
3import scala.concurrent.Await
4import scala.concurrent.duration.Duration
5 
6object UserApp {
7  def main(args: Array[String]): Unit = {
8    implicit val system = ActorSystem("myuser")
9 
10    system.actorOf(Props(new Master), "user-app-master")
11 
12    Await.ready(system.whenTerminated, Duration.Inf)
13  }
14}
15 
16class Master extends Actor with ActorLogging with ActorSettings {
17  override val supervisorStrategy = SupervisorStrategy.stoppingStrategy
18 
19  private val userRepository = context.watch(createUserRepository())
20  context.watch(createHttpService(userRepository))
21 
22  log.info("Up and running")
23 
24  override def receive = {
25    case Terminated(actor) => onTerminated(actor)
26  }
27 
28  protected def createUserRepository(): ActorRef = {
29    context.actorOf(UserRepository.props(), UserRepository.Name)
30  }
31 
32  protected def createHttpService(userRepositoryActor: ActorRef): ActorRef = {
33    import settings.httpService._
34    context.actorOf(
35      HttpService.props(address, port, selfTimeout, userRepositoryActor),
36      HttpService.Name
37    )
38  }
39 
40  protected def onTerminated(actor: ActorRef): Unit = {
41    log.error("Terminating the system because {} terminated!", actor)
42    context.system.terminate()
43  }
44}

As you can see in the code, it also initializes the UserRepository Actor. This Actor will function as a ‘stub’ for our persistent storage or other business logic. It doesn’t really matter for this demo application, just that we have some place where we can store and retrieve user records. Here is the code for UserRepository.scala:

1import akka.actor.{ Props, ActorLogging }
2 
3object UserRepository {
4 
5  case class User(name: String)
6  case object GetUsers
7  case class AddUser(name: String)
8  case class UserAdded(user: User)
9  case class UserExists(name: String)
10 
11  final val Name = "user-repository"
12  def props(): Props = Props(new UserRepository())
13}
14 
15class UserRepository extends ActorLogging {
16  import UserRepository._
17 
18  private var users = Set.empty[User]
19 
20  override def receive = {
21    case GetUsers =>
22      log.debug("received GetUsers command")
23      sender() ! users
24    case AddUser(name) if users.exists(_.name == name) =>
25      sender() ! UserExists(name)
26    case AddUser(name) =>
27      log.info(s"Adding new user with name; $name")
28      val user = User(name)
29      users += user
30      sender() ! UserAdded(user)
31  }
32}

And then finally the HTTP Service. I’m going to show you here a stripped-down version, to later show the full version with all annotations etc. necessary for Swagger.

1object HttpService {
2 
3   // $COVERAGE-OFF$
4  final val Name = "http-service"
5  // $COVERAGE-ON$
6 
7  def props(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef): Props =
8    Props(new HttpService(address, port, internalTimeout, userRepository))
9 
10  private def route(httpService: ActorRef, address: String, port: Int, internalTimeout: Timeout,
11    userRepository: ActorRef, system: ActorSystem)(implicit ec: ExecutionContext, mat: Materializer) = {
12    import Directives._
13    import io.circe.generic.auto._
14 
15    new UserService(userRepository, internalTimeout).route
16  }
17}
18 
19class HttpService(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef)
20    extends Actor with ActorLogging {
21  import HttpService._
22  import context.dispatcher
23 
24  private implicit val mat = ActorMaterializer()
25 
26  Http(context.system)
27    .bindAndHandle(route(self, address, port, internalTimeout, userRepository, context.system), address, port)
28    .pipeTo(self)
29 
30  override def receive = binding
31 
32  private def binding: Receive = {
33    case serverBinding @ Http.ServerBinding(address) =>
34      log.info("Listening on {}", address)
35 
36    case Status.Failure(cause) =>
37      log.error(cause, s"Can't bind to $address:$port")
38      context.stop(self)
39  }
40}
41 
42class UserService(userRepository: ActorRef, internalTimeout: Timeout)(implicit executionContext: ExecutionContext) extends Directives {
43  import CirceSupport._
44  import io.circe.generic.auto._
45 
46  implicit val timeout = internalTimeout
47 
48  val route = pathPrefix("users") { usersGetAll ~ userPost }
49 
50  def usersGetAll = get {
51    complete {
52      (userRepository ? UserRepository.GetUsers).mapTo[Set[UserRepository.User]]
53    }
54  }
55 
56  def userPost = post {
57    entity(as[UserRepository.User]) { user =>
58      onSuccess(userRepository ? UserRepository.AddUser(user.name)) {
59        case UserRepository.UserAdded(_)  => complete(StatusCodes.Created)
60        case UserRepository.UserExists(_) => complete(StatusCodes.Conflict)
61      }
62    }
63  }
64}

What is most important, is that you split up the HTTP Service and especially our UserService into separate classes. This will let us annotate the UserService class separately, only exposing this one for Swagger. Should you have multiple services, then splitting them up in the right way will also make sure the operations are nicely grouped.

Swagger Setup

Now finally to the part where we configure Swagger to generate the API documentation.

Serve Swagger UI Resources

In one of the first steps, we added all resources for the Swagger UI. To serve them, we need to tell Akka HTTP where and how to serve these files. Luckily this is very easy and requires only two lines in our HttpService class (remember, I’ll show the full class at the end):

1def assets = pathPrefix("swagger") {
2      getFromResourceDirectory("swagger") ~ pathSingleSlash(get(redirect("index.html", StatusCodes.PermanentRedirect))) }
3 
4assets ~ new UserService(userRepository, internalTimeout).route

The first line tells Akka HTTP where it can find the necessary files, and that we want them served on the path /swagger. The second line adds this route definition to the total routes definition.

Swagger Annotations

First of all on the service level, we need to use the @Api annotation. This will tell Swagger this is an entry point for a REST service, and the path it maps to. For example:

1@Api(value = "/users", produces = "application/json")

The annotation for the GET request is also very easy. For this we use the @ApiOperation annotation. Most noteworthy here is the response and responseContainer attributes we have to supply. The response points to the class of the object that is being returned. It lets Swagger correctly show example responses (and later for the POST create example requests). The responseContainer is required when a list or set is returned, to describe the correct container type (and not to ‘pollute’ the object return type).

1@ApiOperation(value = "Get list of all users", nickname = "getAllUsers", httpMethod = "GET",
2    response = classOf[UserRepository.User], responseContainer = "Set")

Lastly the annotation for the POST request. Again the @ApiOperation annotation is used, but now we also need the @ApiImplicitParams and @ApiResponses annotations. This last annotation is just to notify we may return other HTTP response codes besides 200 OK, and the reason why this response code might be returned.
The @ApiImplicitParams annotation describes the parameter supplied with the HTTP POST. Also here you need to point to a class so that Swagger can correctly describe the object. Strangely, where for the GET request @ApiOperation a class object was required, now the dataType attribute requires a string object.

1@ApiOperation(value = "Create new user", nickname = "userPost", httpMethod = "POST", produces = "text/plain")
2  @ApiImplicitParams(Array(
3    new ApiImplicitParam(name = "user", dataType = "UserRepository$User", paramType = "body", required = true)
4  ))
5  @ApiResponses(Array(
6    new ApiResponse(code = 201, message = "User created"),
7    new ApiResponse(code = 409, message = "User already exists")
8  ))

For a full description of all Swagger annotations see https://github.com/swagger-api/swagger-core/wiki/Annotations-1.5.X

Last thing to note, apparently the javax.ws.rs.Path is always necessary for Swagger to detect the service. So while it has no use for Akka HTTP, you need to include it for Swagger.

Swagger HTTP Service

The SwaggerHttpService is a trait that needs to be included to actually generate and provide the JSON API documentation described above. Most importantly, you need to point it to your REST services, in our case the UserService. The full class is described below:

1import com.github.swagger.akka.model.Info
2import nl.codecentric.UserService
3 
4import scala.reflect.runtime.{ universe => ru }
5import akka.actor.ActorSystem
6import akka.stream.ActorMaterializer
7import com.github.swagger.akka._
8 
9class SwaggerDocService(address: String, port: Int, system: ActorSystem) extends SwaggerHttpService with HasActorSystem {
10  override implicit val actorSystem: ActorSystem = system
11  override implicit val materializer: ActorMaterializer = ActorMaterializer()
12  override val apiTypes = Seq(ru.typeOf[UserService])
13  override val host = address + ":" + port
14  override val info = Info(version = "1.0")
15}

And then we need to include it in the Akka HTTP route definition:

1assets ~ new UserService(userRepository, internalTimeout).route ~ new SwaggerDocService(address, port, system).routes

CORS Support

One last thing we need is to configure HTTP cross-origin access control. To make sure the Swagger UI works with our service when running on a different domain or on localhost. The CorsSupport class I included in my project is taken directly from https://github.com/pjfanning/swagger-akka-http-sample . It is this class: https://github.com/pjfanning/swagger-akka-http-sample/blob/master/src/main/scala/com/example/akka/CorsSupport.scala.
Now just to hook it up into our project:

1object HttpService extends CorsSupport {
2 
3...
4 
5    assets ~ corsHandler(new UserService(userRepository, internalTimeout).route) ~ corsHandler(new SwaggerDocService(address, port, system).routes)

Full HTTP Service Class

And finally the full HttpService class with the service, the annotations, CORS support etc.:

1import javax.ws.rs.Path
2 
3import akka.actor._
4import akka.http.scaladsl.Http
5import akka.http.scaladsl.model.StatusCodes
6import akka.http.scaladsl.server.Directives
7import akka.stream.{ Materializer, ActorMaterializer }
8import akka.pattern.{ ask, pipe }
9import akka.util.Timeout
10import de.heikoseeberger.akkahttpcirce.CirceSupport
11import nl.codecentric.user.swagger.SwaggerDocService
12import nl.codecentric.user.util.CorsSupport
13import scala.concurrent.ExecutionContext
14import io.swagger.annotations._
15 
16object HttpService extends CorsSupport {
17  // $COVERAGE-OFF$
18  final val Name = "http-service"
19  // $COVERAGE-ON$
20 
21  def props(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef): Props =
22    Props(new HttpService(address, port, internalTimeout, userRepository))
23 
24  private def route(httpService: ActorRef, address: String, port: Int, internalTimeout: Timeout,
25    userRepository: ActorRef, system: ActorSystem)(implicit ec: ExecutionContext, mat: Materializer) = {
26    import Directives._
27    import io.circe.generic.auto._
28 
29    // format: OFF
30    def assets = pathPrefix("swagger") {
31      getFromResourceDirectory("swagger") ~ pathSingleSlash(get(redirect("index.html", StatusCodes.PermanentRedirect))) }
32 
33    assets ~ corsHandler(new UserService(userRepository, internalTimeout).route) ~ corsHandler(new SwaggerDocService(address, port, system).routes)
34  }
35}
36 
37class HttpService(address: String, port: Int, internalTimeout: Timeout, userRepository: ActorRef)
38    extends Actor with ActorLogging {
39  import HttpService._
40  import context.dispatcher
41 
42  private implicit val mat = ActorMaterializer()
43 
44  Http(context.system)
45    .bindAndHandle(route(self, address, port, internalTimeout, userRepository, context.system), address, port)
46    .pipeTo(self)
47 
48  override def receive = binding
49 
50  private def binding: Receive = {
51    case serverBinding @ Http.ServerBinding(address) =>
52      log.info("Listening on {}", address)
53 
54    case Status.Failure(cause) =>
55      log.error(cause, s"Can't bind to $address:$port")
56      context.stop(self)
57  }
58}
59 
60@Path("/users")  // @Path annotation required for Swagger
61@Api(value = "/users", produces = "application/json")
62class UserService(userRepository: ActorRef, internalTimeout: Timeout)(implicit executionContext: ExecutionContext) extends Directives {
63  import CirceSupport._
64  import io.circe.generic.auto._
65 
66  implicit val timeout = internalTimeout
67 
68  val route = pathPrefix("users") { usersGetAll ~ userPost }
69 
70  @ApiOperation(value = "Get list of all users", nickname = "getAllUsers", httpMethod = "GET",
71    response = classOf[UserRepository.User], responseContainer = "Set")
72  def usersGetAll = get {
73    complete {
74      (userRepository ? UserRepository.GetUsers).mapTo[Set[UserRepository.User]]
75    }
76  }
77 
78  @ApiOperation(value = "Create new user", nickname = "userPost", httpMethod = "POST", produces = "text/plain")
79  @ApiImplicitParams(Array(
80    new ApiImplicitParam(name = "user", dataType = "nl.codecentric.UserRepository$User", paramType = "body", required = true)
81  ))
82  @ApiResponses(Array(
83    new ApiResponse(code = 201, message = "User created"),
84    new ApiResponse(code = 409, message = "User already exists")
85  ))
86  def userPost = post {
87    entity(as[UserRepository.User]) { user =>
88      onSuccess(userRepository ? UserRepository.AddUser(user.name)) {
89        case UserRepository.UserAdded(_)  => complete(StatusCodes.Created)
90        case UserRepository.UserExists(_) => complete(StatusCodes.Conflict)
91      }
92    }
93  }
94}

Disclaimer

The examples I showed you here are loosely based on Heiko Seeberger’s reactive-flows project regarding the Akka HTTP setup. A demo project can be easily generated with SBT-Fresh sbt-fresh .

Hope you liked this blog and would love to hear your comments!

share post

Likes

0

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.