Download Build Status Codacy Badge

Getting Started

Wiro is published on Bintray and cross-built for Scala 2.11, and Scala 2.12.

How to add dependency:

libraryDependencies ++= Seq(
  "io.buildo" %% "wiro-http-server" % "X.X.X",
  "org.slf4j" % "slf4j-nop" % "1.6.4"
)

Wiro uses scala-logging, so you need to include an SLF4J backend. We include slf4j-nop to disable logging, but you can replace this with the logging framework you prefer (log4j2, logback).

Wiro uses scala macro annotations. You’ll also need to include the Macro Paradise compiler plugin in your build:

addCompilerPlugin(
  "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full
)

Why Wiro?

Wiro is a lightweight Scala library to automatically generate http routes from scala traits. At buildo, we don’t like writing routes. Writing routes is an error-prone and frustrating procedure. Futhermore, in most of our use-cases, it can be completely automatized.

Features

Here is the list of the most relevant features of Wiro:

  • Automatic generation of http routes from decorated Scala traits
  • Automatic generation of an http client that matches the generated routes
  • Extensible error module (based on type classes)
  • Custom routes definition (in case you want to use http methods or write paths that Wiro doesn’t provide)
  • Support for HTTP authorization header

Step by Step Example

In this example we will build an api to store and retrieve users.

1 - Model and Controller Interface

First, let’s start by defining the interface of the API controller. We want the API to support the two following operations:

  1. The client should be able to insert a new user by specifying id and name
  2. The client should be able to retrieve a user by id

The following snippet defines a model for User and an interface that follows this specification.

import scala.concurrent.Future
import wiro.annotation._

// Models definition
case class User(name: String)

// Error messages
case class Error(msg: String)
case class UserNotFoundError(msg: String)

@path("users")
trait UsersApi {

  @query
  def getUser(
    id: Int
  ): Future[Either[UserNotFoundError, User]]

  @command
  def insertUser(
    id: Int,
    name: String
  ): Future[Either[Error, User]]
}
  • Use the @query and @command annotations to handle GET and POST requests respectively.
  • Return types must be Future of Either like above

2 - Controller implementation

Now that we have the API interface, we need to implement it. Let’s add the following implementation inside the controllers object:

val users = collection.mutable.Map.empty[Int, User] // Users DB

// API implementation
class UsersApiImpl() extends UsersApi {
  import scala.concurrent.ExecutionContext.Implicits.global

  override def getUser(
    id: Int
  ): Future[Either[UserNotFoundError, User]] = {
    users.get(id) match {
      case Some(user) => Future(Right(user))
      case None => Future(Left(UserNotFoundError("User not found")))
    }
  }

  override def insertUser(
    id: Int,
    name: String
  ): Future[Either[Error, User]] = {
    val newUser = User(name)
    users(id) = newUser
    Future(Right(newUser))
  }
}

3 - Serialization and deserialization

Requests and responses composed only by standard types can be serialized and deserialized automatically by circe, thanks to the following import:

import io.circe.generic.auto._

We need, however, to specify how we want the errors that we defined to be converted into HTTP responses. To do it, it is sufficient to define the corresponding implicits, like we do in the following snippet:

import wiro.server.akkaHttp.ToHttpResponse
import wiro.server.akkaHttp.FailSupport._

import akka.http.scaladsl.model.{ HttpResponse, StatusCodes, ContentType, HttpEntity}
import akka.http.scaladsl.model.MediaTypes

import io.circe.syntax._
implicit def notFoundToResponse = new ToHttpResponse[UserNotFoundError] {
  def response(error: UserNotFoundError) = HttpResponse(
    status = StatusCodes.NotFound,
    entity = HttpEntity(ContentType(MediaTypes.`application/json`), error.asJson.noSpaces)
  )
}

implicit def errorToResponse = new ToHttpResponse[Error] {
  def response(error: Error) = HttpResponse(
    status = StatusCodes.InternalServerError,
    entity = HttpEntity(ContentType(MediaTypes.`application/json`), error.asJson.noSpaces)
  )
}

4 - Router creation

Now we have everything we need to instance and start the router:

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer

import wiro.Config
import wiro.server.akkaHttp._

object UserServer extends App with RouterDerivationModule {
  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()
  implicit val ec = system.dispatcher

  val usersRouter = deriveRouter[UsersApi](new UsersApiImpl)

  val rpcServer = new HttpRPCServer(
    config = Config("localhost", 8080),
    routers = List(usersRouter)
  )
}

5 - Requests examples

Inserting a user:

curl -XPOST 'http://localhost:8080/users/insertUser' \
-d '{"id":0, "name":"Pippo"}' \
-H "Content-Type: application/json"

>> {"name":"Pippo"}

Getting a user:

curl -XGET 'http://localhost:8080/users/getUser?id=0'

>> {"name":"Pippo"}

6 - Wiro client

With wiro you can also create clients and perform requests:

import wiro.client.akkaHttp._
import autowire._

object UserClient extends App with ClientDerivationModule {
  import FailSupport._

  val config = Config("localhost", 8080)

  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()
  implicit val ec = system.dispatcher

  val rpcClient = new RPCClient(config, deriveClientContext[UsersApi])
  rpcClient[UsersApi].insertUser(0, "Pippo").call() map (println(_))
}

7 - Testing

To write tests for the router you can use the Akka Route Testkit. The router to be tested can be extracted from the object that we defined above, as follows:

import org.scalatest.{ Matchers, FlatSpec }
import akka.http.scaladsl.testkit.ScalatestRouteTest
import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._

class UserSpec extends FlatSpec with Matchers with ScalatestRouteTest with RouterDerivationModule {
  val route = deriveRouter[UsersApi](new UsersApiImpl).buildRoute

  it should "get a user" in {
    Get("/users/getUser?id=0") ~> route ~> check {
      responseAs[User].name shouldBe "Pippo"
    }
  }
}

Authorization

How do I authorize my routes? just add a Auth argument to your methods.

Wiro takes care of extracting the token from “Authorization” http header in the form Token token=${YOUR_TOKEN}. The token will automagically passed as token argument.

For example,

import akka.http.scaladsl.model.{ HttpResponse, StatusCodes }
import scala.concurrent.Future
import wiro.Auth
import wiro.annotation._
import wiro.server.akkaHttp._

case class Unauthorized(msg: String)
implicit def unauthorizedToResponse = new ToHttpResponse[Unauthorized] {
  def response(error: Unauthorized) = HttpResponse(
    status = StatusCodes.Unauthorized,
    entity = "canna cross it"
  )
}

@path("users")
trait UsersApi {

  @query
  def getUser(
    token: Auth,
    id: Int
  ): Future[Either[Unauthorized, String]]

  @command
  def insertString(
    token: Auth,
    id: Int,
    name: String
  ): Future[Either[Unauthorized, String]]
}