4. Creating a REST Server with Liberator

In this chapter we will build a more advanced REST server in Liberator. We’ll also look at the Richardson Maturity Model for web services.

Assumptions

In this chapter we assume the following:

Image You have Leiningen version 2 installed and on your path on the command line.

Image You know how to use curl (whether on a Mac or a PC) and have it on your path.

Image You understand the concept of http parameters being passed via a GET request URI and POST request form parameters request.

Benefits

The benefit of this chapter is that you will learn a more fine-grained way to build a REST server.

The Recipe—Code

Follow these steps to create the recipe.

1. Create a new Compojure project called rest-media-demo.

lein new compojure rest-media-demo
cd rest-media-demo

2. Modify the file project.clj to look like this:

(defproject rest-media-demo "0.1.0-SNAPSHOT"
  :min-lein-version "2.0.0"
  :dependencies [[org.clojure/clojure "1.7.0-beta2"]
                 [compojure "1.3.4"]
                 [ring/ring-defaults "0.1.5"]
                 [liberator "0.13"]
                 [org.clojure/data.json "0.2.6"]]
  :plugins [[lein-ring "0.9.5"]]
  :ring {:handler rest-media-demo.handler/app
          :auto-reload? true
          :auto-refresh? true}
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [ring/ring-mock "0.2.0"]]}})

3. Modify the file src/rest_media_demo/handler.clj to look like this:

 (ns rest-media-demo.handler
  (:use compojure.core)
  (:require [compojure.handler :as handler]
            [compojure.route :as route]
            [liberator.core :refer  [resource defresource]]
            [liberator.representation :refer [render-map-generic]]
            [hiccup.core :refer [html]]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
            [clojure.data.json :as json]))


(def default-media-types
  ["application/json"
   "text/plain"
   "text/html"])

(defmethod render-map-generic "application/json" [data context]
  (json/write-str (conj (:links data) (:properties data) )))

(defmethod render-map-generic "text/html" [data context]
  (html [:div
         [:h1 (-> data :class first)]
         [:dl
          (mapcat (fn [[key value]] [[:dt key] [:dd value]])
               (:properties data))]]))


(defrecord Coffee [name price])

(def ristretto
  (->Coffee "Ristretto" "$1.80"))

(def macchiato
  (->Coffee "Macchiato" "$1.80"))

(def latte
  (->Coffee "Caffe Latte" "$3.20"))

(def coffees [latte macchiato ristretto])

(defn index-data [ctx]
  coffees)

(defresource index
  :allowed-methods [:options :get :post  :delete]
  :available-media-types default-media-types
  :handle-ok index-data)


(defresource coffee [id]
  :allowed-methods [:options :get]
  :available-media-types ["text/html" "application/json"]
  :handle-ok (fn [_]
               {:properties
                    (nth coffees (Integer/parseInt id))
                :links [{:rel ["self"]
                              :href (str "/coffees/" id)}
                        {:rel ["listing"]
                         :href "/coffees"}]}))

(defroutes app-routes
  (OPTIONS "/" []
         {:headers {"Allow:" "GET, POST, DELETE, OPTIONS"}})
  (ANY "/" [] index)
  (ANY "/coffees"[] index)
  (OPTIONS "/coffees/:id" []
         {:headers {"Allow:" "GET, OPTIONS"}})
  (ANY "/coffees/:id" [id]
         (coffee id))
  (route/resources "/")
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))

Testing the Solution

Now we’ll test it.

1. On the command line start the ring server with Leiningen:

lein ring server 4000

This should prompt your web browser to open and show a page like that shown in Figure 4.1.

Image

Figure 4.1 Index page for REST application in Liberator

2. Now change the URL to http://localhost:4000/coffees/1. You should see the result shown in Figure 4.2.

Image

Figure 4.2 Looking at a page for a record in Liberator

What we see here is the HTML view of the REST data using Liberator’s default HTML layout function. We’ve also defined responses when the client has an application type of JSON. We’ll look at that now.

3. Open a new command prompt and run the following command:

curl -v -H "Accept: application/json" http://localhost:4000/coffees/1

In this command we do an http request with an application/json request header. This tells the application that this is a JSON client and that it should respond accordingly.

In all the result data you should see the following snippet:

[{"rel":["self"], "href":"/coffees/1"}, {"href":"/coffees",
"rel":["listing"]}, {"name":"Macchiato", "price":"$1.80"}]

We can see our result for this record. We also see hypermedia information. We see a link to the record itself under the self reference. We also see a reference to the parent list under listing.

You can try other queries as well.

Notes on the Recipe

There are some things to note about the project.clj file. You’ll notice we’ve added the liberator library. This is a library on top of Compojure to make defining REST services easier.

We’ve also added the data.json library for responding to requests with a JSON media-type http header.

The other thing to notice is the auto-reload? and auto-refresh? keys, which allow us to modify the source code without restarting the application. Each request can recompile the application.

Now look at the file src/rest_media_demo/handler.clj, starting with the namespace declaration. The namespace definition here contains the standard Compojure libraries added by the Compojure Leiningen template. We have added two Liberator libraries, liberator.core and liberator.representation. The Liberator Core libraries enable us to define http resources as a first-class construct. In theory Liberator allows these resources to be queried for the HTTP OPTIONS, media types, and links they support. The Liberator Representations library is used for responding to different media types, enabling you to write different presentation functions for JSON and HTML. The Hiccup library is used when we’re writing our function for presenting to HTML clients. The clojure.data.json library is used when we’re writing a function for presenting to JSON clients.

 (ns rest-media-demo.handler
  (:use compojure.core)
  (:require [compojure.handler :as handler]
            [compojure.route :as route]
            [liberator.core :refer  [resource defresource]]
            [liberator.representation :refer [render-map-generic]]
            [hiccup.core :refer [html]]
            [clojure.data.json :as json]))

Next the handler defines the default media types that the REST application will support. You can see JSON, plain text, and HTML.

(def default-media-types
  ["application/json"
   "text/plain"
   "text/html"])

Next the handler has a multimethod implementation for rendering JSON property sets returned from a Liberator resource. You can see that the defmethod indicates this is an implementation of a multimethod switched on media type. This implements a multimethod defined in the Liberator library. The parent Liberator library multimethod looks like this:1

1. https://github.com/clojure-liberator/liberator/blob/master/src/liberator/representation.clj

 (defmulti render-map-generic "dispatch on media type"
  (fn [data context] (get-in context [:representation :media-type])))

The keys for links and properties put the values out of the data map. These are then conjoined together and fed into the JSON write-str function.

(defmethod render-map-generic "application/json" [data context]
  (json/write-str (conj (:links data) (:properties data) )))

Next the handler has a multimethod implementation for rendering HTML property sets returned from a Liberator resource. Here we’re expecting to get a record stored in the :properties key. The big idea is to break it into key-value pairs for the field name and values, and map this to an HTML definition list.

(defmethod render-map-generic "text/html" [data context]
  (html [:div
         [:h1 (-> data :class first)]
         [:dl
          (mapcat (fn [[key value]] [[:dt key] [:dd value]])
               (:properties data))]]))

Next the handler defines the data. We’ll define a Clojure record, create three instances of that record, and then place these into a vector. The theme is café culture.

(defrecord Coffee [name price])

(def ristretto
  (->Coffee "Ristretto" "$1.80"))

(def macchiato
  (->Coffee "Macchiato" "$1.80"))

(def latte
  (->Coffee "Caffe Latte" "$3.20"))

(def coffees [latte macchiato ristretto])

You can see we’ve created three instances of a record and stored them in a vector. Next the handler adds a wrapper function for Liberator to be able to make reference to the coffees vector.

(defn index-data [ctx]
  coffees)

Next the handler defines a resource for the index request. You can see we’ve defined some allowed methods and available media types. In theory, the allowed-methods would provide a response to an HTTP OPTIONS query, as well as defining which http requests this responds to. The available media types define which types of clients this resource will respond to. The handle-ok keyword returns a function for the HTTP 200 response. In this case we’re pointing to the index-data function, which returns our coffees vector. In this case, Liberator’s default Representation library will be called to render this in HTML or JSON since we haven’t specified a render map for this.

(defresource index
  :allowed-methods [:options :get :post  :delete]
  :available-media-types default-media-types
  :handle-ok index-data)

Next the handler defines the coffee resource. You’ll notice in the resource definition we’ve specified an id parameter. This is passed in from the routes definition from the URL. We’ve defined allowed http methods and available media types. In our handle-ok keyword we define a function that returns both data and links. In the properties keyword we look up the coffee request based on an index in the vector. In the links keyword we provide hypermedia links to provide more information about this resource and how it relates to other resources.

(defresource coffee [id]
  :allowed-methods [:options :get]
  :available-media-types ["text/html" "application/json"]
  :handle-ok (fn [_]
               {:properties (nth coffees (Integer/parseInt id))
                :links [{:rel ["self"] :href (str "/coffees/" id)}
                        {:rel ["listing"] :href "/coffees"}]}))

Next the handler defines the routes. As at the time of writing, the Liberator library wasn’t automatically generating responses to an HTTP OPTIONS request,2 so we’ve put them in manually. The route on the first line defines a response to an HTTP OPTIONS request for the apps index resource.

2. See the bug description here: https://github.com/clojure-liberator/liberator/pull/67.

The second route defines a response for any http verb for the index and returns the index resource, which returns our coffees vector.

In the third line we define a route for any http verb for our coffees resource without an id parameter. This returns the same result as the index.

On the fourth line we define OPTIONS for our coffees resource with an id parameter.

(defroutes app-routes
  (OPTIONS "/" [] {:headers {"Allow:" "GET, POST, DELETE, OPTIONS"}})
  (ANY "/" [] index)
  (ANY "/coffees"[] index)
  (OPTIONS "/coffees/:id" [] {:headers {"Allow:" "GET, OPTIONS"}})
  (ANY "/coffees/:id" [id] (coffee id))
  (route/resources "/")
  (route/not-found "Not Found"))

Context

REST has its origins in the http specification, which has led to some confusion over precisely what it is.

Origins

Roy Fielding was one of the authors of the HTTP specification and in 2000 wrote a dissertation entitled Architectural Styles and the Design of Network-Based Software Architectures in which he described the concept of Representational State Transfer (REST). In a sense, this was a response to the complexity of the Simple Object Access Protocol (SOAP) and Roy Fielding’s perception that the HTTP protocol had been misunderstood as a mere transport protocol.

In terms of client implementation, we can talk about REST as a “URL style.” Suppose we wanted to request the fifth photo of a user uniquely named Bob. Using HTTP GET form parameters, we might write a URL like

http://exampleserver.com/photos?user=Bob&id=5

But using a RESTful style, we would write

http://exampleserver.com/photos/Bob/5

You can see that REST isn’t a protocol on its own—it is a usage pattern of the HTTP protocol. You can read more about this distinction in the Richardson Maturity Model.3

3. http://martinfowler.com/articles/richardsonMaturityModel.html

REST Hypermedia

Some have mistakenly thought that this makes any exposed http interface a REST API. Now I’m sure you can hear Roy Fielding say, “I am getting frustrated by the number of people calling any http-based interface a REST API.”4 The truth is more nuanced that this, and someone at this point will direct you to the Richardson Maturity Model for REST, which looks something like this:

4. http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

Image

What Roy Fielding is saying is that Level 3 is a precondition of REST. What we’ve done is to build a new REST project in Clojure that better achieves this.

Conclusion

We’ve implemented REST hypermedia controls in Liberator.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.88.110