ハツカネズミの恋

Lisp のもろもろ,おぼえがき

How to deal with Ring

What's Ring ?

Ring is a handy set of interfaces to interact with the web server. Ring uses maps instead of functions to express the requests from client and the responses from server, like Rack in Ruby or WSGI in Python. Ring has been used commonly and playing an essential role in Clojure web development .

Components

Ring is composed of :

  • handler
  • middleware
  • request-map
  • response-map

Handler

Handler is a sort of function that takes request-map and return response-map. Handlers in Ring can be implemented as simple function, so they are easily tested.

(defn handler [req]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr req)})

:status is HTTP status like 404, :headers stands for HTTP header for client, and :body represents the response body.

Middleware

Middleware is defined as a higher-order-function for handlers. Middleware should take a handler as the first argument and return a new handler.

You can write middleware like:

(defn wrap-question-mark [handler]
  (fn [request]
    (let [response (handler request)]
      (update response :body #(str % "??")))))

I know this sample is a completely-nonsense, but sometimes you want to add double-question mark to every single response body, do you ??

Libraries

Ring has 4 libraries:

  • ring-core
  • ring-devel
  • ring-jetty-adapter
  • ring-servlet

Jetty is a Java HTTP server and Java Servlet container.

Eval in REPL

Now, let's eval next lines in your REPL ($ lein repl or something) Firstly, you need to require ring-jetty-adapter to connect Ring app to Jetty.

user => (require '[ring.adapter.jetty :as s])
;=> nil

This will return nil and it's a sign that you've successfully connected your app to Jetty.

Then, you can do next step: define the status of your server to (atom nil). Atoms provide a shared, synchronous state, and enables you to make synchronous changes of value. Since, In this case, the state of server would be toggled, you should use atom.

user => (def server (atom nil))
;=> #'user/server

You will get #'user/server as a result. To the finish, reset the server to start your server in port 3000.

user => (reset! server (s/run-jetty (fn [req] {:body "Close the world, Open the nExt"}) {:port 3000 :join? false}))

Let's explain these lines.

  1. reset! sets the instance returned by s/run-jetty to the server.
    This means the value of (atom nil) has been changed.
  2. s/run-jetty takes 2 arguments:
    first one ((fn [req] {:body "Close the world, Open the nExt"})) is a Ring handler function taking the request-map and returning the response-map, and the second ({:port 3000 :join? false}) is a server option.

Now go to your browser and type http://localhost:3000/. If there is no mistake, the text "Hello, World" should be displayed.

If you get tired of playing with your "Close the world", then you should stop the server properly. Eval next lines on your REPL.

user=> (.stop @server)
; => nil

user=> (reset! server nil)
; => nil

This will stop the server.

  1. In (.stop @server) , .stop is an instance method and it works literally. @server is a reference to server which has been bound to an atom.
  2. reset changes the state of server to nil again.

Finishing Touches

It's time to summarise the progress so far and write it to a file.

(ns example-clj.core
  (:require [ring.adapter.jetty :as server]))

(defonce server (atom nil))

(defn handler [req]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Close the world, Open the nExt"})

(defn start-server []
  (when-not @server
    (reset! server (server/run-jetty handler {:port 3000 :join? false}))))

(defn stop-server []
  (when @server
    (.stop @server)
    (reset! server nil)))

(defn restart-server []
  (when @server
    (stop-server)
    (start-server)))

Well, it seems like too long to see at once but meaning of each is quite simple. I'll explain these definitions in order.

  1. (defonce server (atom nil)) sets the state of server using atom. Using defonce prevent from redefining server when you reload the file.
  2. handler is as described earlier. What was previously defined as (fn [req] {:body "Hello, world"}) {:port 3000 :join? false}) is redefined as .
  3. start-server takes no args. when-not is an unless-like macro. If the @server has no value (= nil), the state of server is reset to the result of (server/run-jetty handler {:port 3000 :join? false}).
  4. stop-server takes no args. when is a macro means if .. do... When the @server has some value(= be evaluated "logical-true"), then (.stop @server) is executed. Otherwise Clojure execute (reset! server nil) and the server will stop.
  5. restart-server is just a composite of start-server and stop-server switched by the current value of @server.

Thus you have finished definition. You now can load the file on REPL and evaluate form like this :

user=> (require '[example-clj.core :as e])
;; => nil
user=> (e/start-server)
;; => #object[org.eclipse.jetty.server.Server 0x55b1143a "org.eclipse.jetty.server.Server@55b1143a"]

See http://localhost:3000/ and then your browser is supposed to display the lovely "Hello, World" text.

Summary

Currently, you've learned how to handle the requests and responses of HTTP with Ring. To the next step, you should get used to routing with Compojure, which is the tool to define lucid routing for Ring.