fwbrasil / zoot   0.14

GNU Library General Public License v2.1 only GitHub

Thin reactive framework to provide and consume REST services

Scala versions: 2.10

Zoot

Gitter Build Status Coverage Status

Thin reactive framework to provide and consume REST services

Artifacts are published on maven central:

http://search.maven.org/#search%7Cga%7C1%7Czoot

Using zoot

Example

Server and client sample implementation:

https://github.com/fwbrasil/zoot/tree/master/zoot-sample/src/main/scala/net/fwbrasil/zoot/sample/counter

Contract

Zoot uses Api traits to define the services 'contract':

trait SomeApi extends Api {
	
	@endpoint(
        method = RequestMethod.PUT,
        path = "/simple")
    def simpleMethod(someInt: Int): Future[Int]
}

Notes:

  1. It is possible to use primitive and non-primitive classes as parameters and return.
  2. Api methods must return Future, otherwise an exception will be thrown.
  3. Apis should always be traits, not classes or abstract classes.
  4. To aggregate Apis, just use common trait inheritance.

Optional parameters

By default all parameters are required and 'BadRequest' is returned if there is a missing parameter.

To define parameters as optional, just use the Option type:

trait SomeApi extends Api {
	
	@endpoint(
        method = RequestMethod.PUT,
        path = "/simple")
    def simpleMethod(someInt: Int, optionalString: Option[String]): Future[Int]
}

In this example, the method will be invoked using 'None' for 'optionalString' if the parameter is missing.

Default parameters

It is possible to define default values for parameters:

trait SomeApi extends Api {
	
	@endpoint(
        method = RequestMethod.PUT,
        path = "/simple")
    def simpleMethod(someInt: Int = 11): Future[Int]
}

If the 'someInt' is not specified in the request parameters, the default value '11' is used.

Parameterized paths

Use parameterized paths to extract parameters from the path:

trait SomeApi extends Api {
	
	@endpoint(
        method = RequestMethod.GET,
        path = "/users/:user/name")
    def userName(user: Int): Future[String]
}

The request for '/users/111/name' will invoke 'userName' using '111' for the 'user' parameter

Response

By default, the response body is the value returned by the method and the http status is '200 Ok'. If the method throws an exception, '500 Internal Server Error' is returned.

It is possible to modify this behavior:

  1. By throwing an ExceptionResponse during the method execution.
  2. By returning a Future[Response].

Client

The Client object provides Api instances for remote services:

val dispatcher: Request => Future[Response[String]] = ???
val client: SomeApi = Client[SomeApi](dispatcher)

You need to provide a 'real' dispatcher instance. Zoot provides implementations for Spray and Finagle.

Once you have the client instance, use Api methods as common method invocations:

val future: Future[Int] = client.simpleMethod(11)
val otherFuture: Future[String] = client.userName(22)

Please refer to the Scala Documentation for more details about Futures.

Server

The Server object allows to create a server using an Api instance.

class SomeService extends SomeApi {
	def simpleMethod(someInt: Int) = Future.success(someInt + 1)
}

val server: Request => Future[Response[String]] = Server[SomeApi](new SomeService)

The server is a function that can be used with the Spray or Finagle bindings.

Mappers

The request and response values are serialized using a StringMapper.

Zoot provides the JacksonStringMapper implementation for json.

Bindings

Please refer to the Spray or Finagle documentation for more details on how to create servers and clients.

Spray

Client

implicit val mirror = scala.reflect.runtime.currentMirror
implicit val mapper = new JacksonStringMapper

val dispatcher = SprayClient(host = "localhost", port = 8080)
val client: SomeApi = Client[SomeApi](dispatcher)

Server

implicit val mirror = scala.reflect.runtime.currentMirror
implicit val mapper = new JacksonStringMapper
implicit val system = ActorSystem("SomeSystem")
implicit val timeout = Timeout(1000 millis)

val server = Server[SomeApi](new SomeService)
val sprayActor = system.actorOf(Props(new SprayServer(server)))

IO(Http) ! Http.Bind(sprayActor, interface = "localhost", port = 8080)

Finagle

Client

implicit val mirror = scala.reflect.runtime.currentMirror
implicit val mapper = new JacksonStringMapper

val builder = ClientBuilder()
    .codec(RichHttp[Request](Http()))
    .hosts(s"$host:$port")
    .hostConnectionLimit(10)
    .requestTimeout(1000 millis)

val dispatcher = FinagleClient(builder.build())
val client: SomeApi = Client[SomeApi](dispatcher)

Server

implicit val mirror = scala.reflect.runtime.currentMirror
implicit val mapper = new JacksonStringMapper

val address = new InetSocketAddress(port)
val builder =
    ServerBuilder()
        .codec(RichHttp[Request](Http()))
        .bindTo(address)
        .keepAlive(true)
        .name("SomeServer")

val server = Server[SomeApi](new SomeService)
val finagleServer = FinagleServer(server, builder.build)

Filters

It is possible to define filters for zoot clients and servers. Example:

val requestLogFilter =
	new Filter {
        override def apply(request: Request, next: Service) = {
            log(s"request $request")
            next(request)
        }

Use the filters when creating a server or client:

val server = requestLogFilter andThen Server[SomeApi](new SomeService)

val client = Client[SomeApi](requestLogFilter andThen dispatcher)

FAQ

Why 'zoot'?

The name is a reference to the Zoot character from The Muppets Show, inspired by the jazz saxophonist Zoot Sims.

https://www.youtube.com/watch?v=CgfZVNv6w2E

"Forgive me Roy Fielding wherever you are!"

Why 'reactive'?

This is the buzzword of the moment and zoot uses non-blocking asynchronous IO.

Should Api files rule the world?

Probably not. :)

If the client and server are using scala and zoot, it is a big win to reuse the Api traits to communicate. If not, just write them by your own. Anyway you need to specify how to invoke services.

Is zoot compatible with Scala.js?

No, since it uses runtime proxy generation. It is possible to remove this limitation using macros. Please open an issue if you would like have Scala.js compatibility.