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.
reset!
sets the instance returned by s/run-jetty to theserver
.
This means the value of(atom nil)
has been changed.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.
- In
(.stop @server)
,.stop
is an instance method and it works literally.@server
is a reference toserver
which has been bound to an atom. reset
changes the state ofserver
tonil
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.
(defonce server (atom nil))
sets the state ofserver
using atom. Usingdefonce
prevent from redefiningserver
when you reload the file.handler
is as described earlier. What was previously defined as(fn [req] {:body "Hello, world"}) {:port 3000 :join? false})
is redefined as .start-server
takes no args.when-not
is an unless-like macro. If the@server
has no value (=nil
), the state ofserver
is reset to the result of(server/run-jetty handler {:port 3000 :join? false})
.stop-server
takes no args.when
is a macro meansif .. 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.restart-server
is just a composite ofstart-server
andstop-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.