Putting It All Together

Let’s look at an example of a service that has a couple of resources that allow the client to read and store some data.

The application will display a list of users and allow the client to add additional users to the list. The client will be implemented in JavaScript and use Ajax to communicate with the service.

To start, let’s create a static HTML page in our public directory and call it home.html. The page contents will look like this:

liberator-snippets/home.html
 
<html>
 
<head>
 
<title>​Liberator Example​</title>
 
<script​ type=​"text/javascript"
 
src=​"//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"​​>
 
</script>
 
 
<script​ type=​"text/javascript"​​>
 
function renderUsers(users) {
 
$('#user-list').empty();
 
for(user in users)
 
$('#user-list').append($('​<li​​/>​', {html: users[user]}));
 
}
 
function getUsers() {
 
$.get("/users", renderUsers);
 
}
 
function addUser() {
 
$.post("/add-user", {user: $('#name').val()}, renderUsers);
 
}
 
 
$(function() {getUsers();});
 
</script>
 
</head>
 
 
<body>
 
<h1>​Current Users​</h1>
 
 
<ul​ id=​"user-list"​​>​​</ul>
 
<input​ type=​"text"​ id=​"name"​ placeholder=​"user name"​​/>
 
<button​ onclick=​"addUser()"​​>​Add User​</button>
 
</body>
 
 
</html>

The page contains functions to render a list of users from given a JSON array, get the current users from the /users URI, and add a new user via the /add-user URI. In addition we have a user-list placeholder for displaying the users, and a text field along with the Add User button for adding new users. The page should look like the following image.

images/screenshots/liberator1.png

We’ll now create corresponding resources to handle each of the operations. To serve the data as JSON we’ll first have to add a reference to cheshire.core/generate-string in the declaration of our home namespace:

 
(​ns​ liberator-service.routes.home
 
(:require ​..​​.
 
[cheshire.core :refer [generate-string]]))

Next we’ll create an atom to hold the list of users:

 
(​def​ users (​atom​ [​"John"​ ​"Jane"​]))

The first resource will respond to GET requests and return the contents of the users atom as JSON.

liberator-service/src/liberator_service/routes/home.clj
 
(defresource get-users
 
:allowed-methods [:get]
 
:handle-ok (​fn​ [_] (generate-string @users))
 
:available-media-types [​"application/json"​])

In the resource, we use the :allowed-methods key to restrict it to only serve GET requests. We use the available-media-types declaration to specify that the response is of type application/json. The resource will generate a JSON string from our current list of users when called.

The second resource will respond to POST and add the user contained in the form-params to the list of users. It will then return the new list:

liberator-snippets/home.clj
 
(defresource add-user
 
:method-allowed? (request-method-in :post)
 
:post!
 
(​fn​ [context]
 
(​let​ [params (​get-in​ context [:request :form-params])]
 
(​swap!​ users ​conj​ (​get​ params ​"user"​))))
 
:handle-created (​fn​ [_] (generate-string @users))
 
:available-media-types [​"application/json"​])

Here we check that the method is POST, and use the post! action to update the existing list of users. We then use the handle-created handler to return the new list of users to the client.

Note that with the resource just detailed, the handle-created value must be a function.

The following resource will compile without errors. However, when it runs you’ll see the old value of users. This is because (generate-string @users) is evaluated before the decision graph is run.

liberator-snippets/home.clj
 
(defresource add-user
 
:method-allowed? (request-method-in :post)
 
:post!
 
(​fn​ [context]
 
(​let​ [params (​get-in​ context [:request :form-params])]
 
(​swap!​ users ​conj​ (​get​ params ​"user"​))))
 
:handle-created (generate-string @users)
 
:available-media-types [​"application/json"​])

It is therefore important to ensure that you provide the :handle-created key with a function that will be run when the decision graph is executed, as we did in the original example.

You’ll notice that nothing is preventing us from adding a blank user. Let’s add a check in our service to validate the request to add a new user:

liberator-service/src/liberator_service/routes/home.clj
 
(defresource add-user
 
:allowed-methods [:post]
 
:malformed? (​fn​ [context]
 
(​let​ [params (​get-in​ context [:request :form-params])]
 
(​empty?​ (​get​ params ​"user"​))))
 
:handle-malformed ​"user name cannot be empty!"
 
:post!
 
(​fn​ [context]
 
(​let​ [params (​get-in​ context [:request :form-params])]
 
(​swap!​ users ​conj​ (​get​ params ​"user"​))))
 
:handle-created (​fn​ [_] (generate-string @users))
 
:available-media-types [​"application/json"​])

Now, if the value of the user parameter is empty, we’ll be routed to handle-malformed, which will inform the client that the user name cannot be empty. Next time we try to add an empty user, we’ll see a 400 error in the browser:

 
POST http://localhost:3000/add-user 400 (Bad Request)

We can now update our page to handle the error and display the message, as follows:

liberator-snippets/home1.html
 
<html>
 
<head>
 
<meta​ http-equiv=​"Content-Type"​ content=​"text/html; charset=US-ASCII"​​>
 
<title>​Liberator Example​</title>
 
 
<script​ type=​"text/javascript"
 
src=​"//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"​​>
 
</script>
 
 
<script​ type=​"text/javascript"​​>
 
function renderUsers(users) {
 
$('#user-list').empty();
 
for(user in users)
 
$('#user-list').append($('​<li​​/>​', {html: users[user]}));
 
}
 
 
function getUsers() {
 
$.get("/users", renderUsers);
 
}
 
 
function handleError(xhr) {
 
$('#error').text(xhr.statusText + ": " + xhr.responseText);
 
}
 
 
function addUser() {
 
var jqxhr = $.post("/add-user", {user: $('#name').val()}, renderUsers)
 
.fail(handleError);
 
}
 
 
$(function() {getUsers();});
 
</script>
 
</head>
 
<body>
 
<h1>​Current Users​</h1>
 
<p​ id=​"error"​​>​​</p>
 
<ul​ id=​"user-list"​​>​​</ul>
 
<input​ type=​"text"​ id=​"name"​ placeholder=​"user name"​​/>
 
<button​ onclick=​"addUser()"​​>​Add User​</button>
 
</body>
 
</html>

Now, if we click the Add User button without filling in the user name field we’ll see the following error:

images/screenshots/liberator2.png

As a final touch, let’s add a home resource that will serve our home.html file. To do that we’ll add the lib-noir dependency to our project.clj:

 
:dependencies [​..​​.​ [lib-noir ​"0.7.2"​]]

Next we’ll add references to noir.io and clojure.java.io to the home namespace declaration:

 
(​ns​ liberator-service.routes.home
 
(:require [​..​​.
 
[noir.io :as io]
 
[clojure.java.io :refer [file]]))

Now we can create a new resource called home that will serve the home.html file:

liberator-service/src/liberator_service/routes/home.clj
 
(defresource home
 
:available-media-types [​"text/html"​]
 
 
:exists?
 
(​fn​ [context]
 
[(io/get-resource ​"/home.html"​)
 
{::file (file (​str​ (io/resource-path) ​"/home.html"​))}])
 
 
:handle-ok
 
(​fn​ [{{{resource :resource} :route-params} :request}]
 
(clojure.java.io/input-stream (io/get-resource ​"/home.html"​)))
 
:last-modified
 
(​fn​ [{{{resource :resource} :route-params} :request}]
 
(​.​lastModified (file (​str​ (io/resource-path) ​"/home.html"​)))))

The resource will check whether the file exists and when it was last modified. If the file isn’t available then io/get-resource will return a nil and the client will get a 404 error. If the file wasn’t changed since the last request, the client will be returned a 304 code instead of the file, indicating that it wasn’t modified.

Thanks to this check, the file will be served only if it exists and we made changes to it since it was last requested. We can now add a route to serve home.html as our default resource:

 
(ANY ​"/"​ request home)

Our home namespace containing the service counterparts to the page should look like this:

liberator-service/src/liberator_service/routes/home.clj
 
(​ns​ liberator-service.routes.home
 
(:require [compojure.core :refer :all]
 
[liberator.core :refer [defresource resource]]
 
[cheshire.core :refer [generate-string]]
 
[noir.io :as io]
 
[clojure.java.io :refer [file]]))
 
(defresource home
 
:available-media-types [​"text/html"​]
 
 
:exists?
 
(​fn​ [context]
 
[(io/get-resource ​"/home.html"​)
 
{::file (file (​str​ (io/resource-path) ​"/home.html"​))}])
 
 
:handle-ok
 
(​fn​ [{{{resource :resource} :route-params} :request}]
 
(clojure.java.io/input-stream (io/get-resource ​"/home.html"​)))
 
:last-modified
 
(​fn​ [{{{resource :resource} :route-params} :request}]
 
(​.​lastModified (file (​str​ (io/resource-path) ​"/home.html"​)))))
 
 
(​def​ users (​atom​ [​"foo"​ ​"bar"​]))
 
(defresource get-users
 
:allowed-methods [:get]
 
:handle-ok (​fn​ [_] (generate-string @users))
 
:available-media-types [​"application/json"​])
 
 
(defresource add-user
 
:allowed-methods [:post]
 
:malformed? (​fn​ [context]
 
(​let​ [params (​get-in​ context [:request :form-params])]
 
(​empty?​ (​get​ params ​"user"​))))
 
:handle-malformed ​"user name cannot be empty!"
 
:post!
 
(​fn​ [context]
 
(​let​ [params (​get-in​ context [:request :form-params])]
 
(​swap!​ users ​conj​ (​get​ params ​"user"​))))
 
:handle-created (​fn​ [_] (generate-string @users))
 
:available-media-types [​"application/json"​])
 
 
(defroutes home-routes
 
(ANY ​"/"​ request home)
 
(ANY ​"/add-user"​ request add-user)
 
(ANY ​"/users"​ request get-users))

As you can see, Liberator ensures separation of concerns by design. With the Liberator model you will have small self-contained functions, each of which handles a specific task.

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

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