Friday, 08 August 2014

Presenting liberator-transit

As part of my prepartion for the core.async workshop I’ll be giving at Strange Loop, I have come across the need for a RESTful API for a web application I am creating. Naturally, I thought this would be a good chance to try out Liberator and Transit. I was pleased to see how easy Liberator is to use. Moreover, it was nearly trivial to add Transit serialisation support to Liberator. Nonetheless, I thought it would be good to wrap it all up in a library ready for reuse, which I have unimaginatively called liberator-transit.

How to use it

Using liberator-transit is straightforward:

  1. Add it to your project as a dependency.
  2. Require the io.clojure.liberator-transit namespace
  3. Accept the application/transit+json and/or application/transit+msgpack media types.

For example, given the following project.clj:

(defproject liberator-transit-demo "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [compojure "1.1.8"]
                 [com.cognitect/transit-clj "0.8.247"]
                 [liberator "0.12.0"]
                 [ring/ring-core "1.3.0"]
                 [io.clojure/liberator-transit "0.1.0"]]
  :plugins [[lein-ring "0.8.11"]]
  :ring {:handler liberator-transit-demo.core/app})

And the given liberator_transit_demo/core.clj:

(ns liberator-transit-demo.core
  (:require [compojure.core :refer [ANY defroutes]]
            [liberator.core :refer [defresource]]
            [ring.middleware.params :refer [wrap-params]]))

(defresource hello [name]
  :available-media-types ["text/plain"
  :handle-ok {:time (System/currentTimeMillis)
  :greeting (str "Hello, " name \!)})

(defroutes routes
  (ANY "/:name" [name] (hello name)))

(def app (wrap-params routes))

You can run the server using lein-ring:

$ lein ring server-headless
2014-08-08 23:12:23.330:INFO:oejs.Server:jetty-7.6.8.v20121106
2014-08-08 23:12:23.349:INFO:oejs.AbstractConnector:Started SelectChannelConnector@
Started server on port 3000

Now, it is possible to see what the different encodings look like:

$ curl http://localhost:3000/text
greeting=Hello, text!
$ curl -H "Accept: application/edn" http://localhost:3000/edn
{:time 1407558307759, :greeting "Hello, edn!"}
$ curl -H "Accept: application/json" http://localhost:3000/json
{"time":1407558370116,"greeting":"Hello, json!"}
$ curl -H "Accept: application/transit+json" http://localhost:3000/transit-json
["^ ","~:time",1407558488590,"~:greeting","Hello, transit-json!"]
$ curl -H "Accept: application/transit+json;verbose" http://localhost:3000/transit-json-verbose
{"~:time":1407558554647,"~:greeting":"Hello, transit-json-verbose!"}
$ curl -s -H "Accept: application/transit+msgpack" http://localhost:3000/transit-msgpack | xxd
0000000: 82a6 7e3a 7469 6d65 cf00 0001 47b9 08d1  ..~:time....G...
0000010: 52aa 7e3a 6772 6565 7469 6e67 b748 656c  R.~:greeting.Hel
0000020: 6c6f 2c20 7472 616e 7369 742d 6d73 6770  lo, transit-msgp
0000030: 6163 6b21                                ack!

There’s just a couple of things to note:

  1. The default Transit/JSON encoding is the non-verbose format. By adding the “verbose” to the “Accept” header, liberator-transit emits the verbose JSON encoding.
  2. Since the MessagePack encoding is a binary format, I pipe the output through xxd. Otherwise, unprintable characters are output.

How it works

Liberator has a built-in mechanism for formatting output of sequences and maps automatically depending on the headers in the request, as seen above. Moreover, it’s possible to extend this easily by adding new methods to the render-map-generic and render-seq-generic multimethods in liberator.representation. These methods dispatch on the media type that has been negotiated and take two arguments: the data to render and the context.

Getting the verbose output to work was just a touch trickier. The parameters to the media type are stripped by Liberator. As a result, it is necessary to actually examine the request headers that were placed in the Ring map. Fortunately, this is easy to do as it is a part of the incoming context.

Here is an example:

(defmethod render-map-generic "application/transit+json"
  [data context]
  (let [accept-header (get-in context [:request :headers "accept"])]
    (if (pos? (.indexOf accept-header "verbose"))
      (render-as-transit data :json-verbose)
      (render-as-transit data :json))))

Further work

This library is really quite simple. I spent far more time creating test.check generators for the various Transit types than I did on the library itself. It mostly exists to provide an even easier way to add Transit to Liberator.

Nonetheless, if other people find a need for it, there is possibly room for improvement, such as:

  1. Is there a better way to handle the request for verbose JSON?
  2. Should the library be configurable? If so, what should be configured and what is the best way to do it?