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:
- The client should be able to insert a new user by specifying
id
andname
- 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 handleGET
andPOST
requests respectively. - Return types must be
Future
ofEither
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]]
}