Search This Blog

Tuesday, January 8, 2013

Getting Started with Ring

In order to write a web app in clojure, it's necessary to understand the library Ring. Ring is a lovely clean design, with good up-to-date docs in the form of its wiki on github.

I'd like to understand how ring works rather better than I do, and so I'm working my way through the whole tutorial step by step, and as I'm going, I'm trying out various things, and making notes.

Here's my version of the 'getting started' tutorial. It's got a few extra frills over the original. I don't know whether other people will find it confusing or helpful, but I'm pretty sure that I'll need to refer to it myself in the future!



;;  necessary dependencies 
;; [[org.clojure/clojure "1.4.0"]
;;  [ring/ring "1.1.6"]]
;; -------------

;; A ring application is a function which takes a request map, and
;; returns a response map

;; Our first response map will have the HTTP status code 200, OK, a
;; content-type header that tells the browser that it's getting plain text
;; and a traditional body text.

(defn app [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello World"})


;; Having got a ring application, we need to start a webserver to hand the pages out
;; We'll use jetty (via ring)
(require 'ring.adapter.jetty)

;; And we'd like those pages served on port 8080 

;; Three things to note here:
;;
;; :join? false means that the evaluating thread won't wait for the
;; server to finish (so that the repl doesn't seem to hang).
;;
;; referring to the application function via #' means that ring sees
;; the variable user/app rather than the function (fn[x]{:status 200})
;; which that variable evaluates to. And that means that if we
;; reevaluate the definition, the behaviour the browser sees will
;; change.
;;
;; finally defonce means that if we reload this file, or re-evaluate
;; this line, nothing will happen. That prevents us from accidentally
;; creating multiple copies of the jetty server.

(defonce server (ring.adapter.jetty/run-jetty #'app {:port 8080 :join? false}))

;; So, go and look at http://localhost:8080 in your favourite browser.



;; Now let's check that redefining the handler causes a change in the running webapp
(defn app [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "<h1>Hello World</h1>"})

;; Refresh your browser to see the change.

;; I like to leave the web browser of the gods:
;; $ watch -d -n 1 curl -sv http://localhost:8080/ 
;; running in a terminal somewhere.

;; Let's demonstrate that we can stop and restart our server

(.stop server)

(.start server)

;; Now, let's look at the information that is going in and out of our application
(require 'clojure.pprint)

;; First we'll delegate the actual functionality of our app to a handler

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "<h1>Hello World</h1>"})

;; And then we'll wrap that in a wrapper that prints the incoming and outgoing data:

(defn app [request]
  (println "-------------------------------")
  (println "Incoming Request:")
  (clojure.pprint/pprint request)
  (let [response (handler request)]
    (println "Outgoing Response Map:")
    (clojure.pprint/pprint response)
    (println "-------------------------------")
    response))

;; Another way to do the same thing is to define what's called a
;; middleware. This is a concept from python, and a good demonstration
;; of why dynamically typed functional languages are such pleasant
;; things to use

;; We define wrap-spy as a function which does to any handler what app does to our handler

(defn wrap-spy [handler]
  (fn [request]
    (println "-------------------------------")
    (println "Incoming Request:")
    (clojure.pprint/pprint request)
    (let [response (handler request)]
      (println "Outgoing Response Map:")
      (clojure.pprint/pprint response)
      (println "-------------------------------")
      response)))


;; And now we can write

(def app 
  (wrap-spy handler))

;; Or more idiomatically

(def app
  (-> handler
      (wrap-spy)))

;; which means exactly the same thing!

;; Unfortunately, we've now lost the ability to redefine handler and see the change 
;; reflected in the running app.
(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "<h1>Hello World!</h1>" })

;; But the same trick with passing the var in works again.
(def app
  (-> #'handler
      (wrap-spy)))

;; And now we do see changes reflected immediately:
(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "<h1>Hello World!!!!!!!!!!!!!!!1</h1>" })

;; Error handling in our app is conservative.

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body (str "<h1>Hello World!!!!!!!!!!!!!!!1</h1>" (/ 1 0))})

;; The browser gets an HTTP 500 Server Error, and the divide by zero
;; message goes to the console where the server is running.

;; But for development purposes, we can use one of the middlewares provided with ring:

(require 'ring.middleware.stacktrace)

(def app
  (-> #'handler
      (ring.middleware.stacktrace/wrap-stacktrace)
      (wrap-spy)))

;; Now the stacktrace appears nicely formatted in the web browser instead.










7 comments:

  1. This is the best ring tutorial I have seen. Keep posting please.

    ReplyDelete
  2. Thanks! Comments like this really mean a lot. I'd be writing the code anyway, but it's a fair bit of extra effort to get it into publishable form and stick it up, and people telling me that they enjoyed it is the incentive I need to do that extra bit.

    ReplyDelete
  3. + 1
    Exactly what I've thought, too. There are so many ways to explain something, but your style fits my way of thinking/understanding very well. I've also enjoyed your cookies post - even if I've no glue why the cookies count twice in Chrome (guess: Chrome is doing multiple requests to speed up the web site rendering process?).
    Please keep up posting!

    Greetings,
    Amdreas

    ReplyDelete
  4. Fantastic tutorial. Good to see you writing again. Back in 2010/2011 I first understood how to write a macro by reading your posts on the subject.

    ReplyDelete
  5. Anonymous writes:

    "I've also enjoyed your cookies post - even if I've no glue why the cookies count twice in Chrome (guess: Chrome is doing multiple requests to speed up the web site rendering process?)"

    Good guess. Chrome actually sends 3 requests, all at once. I ran into this summer and wrote about it here:

    http://www.smashcompany.com/technology/when-speed-is-crucial-you-should-create-your-own-custom-web-server

    Skip down to where I write "Wait, what!?! What the hell just happened?"

    I found the answer here:

    http://stackoverflow.com/questions/4460661/what-to-do-with-chrome-sending-extra-requests

    ReplyDelete
  6. I wondered too. In the trace, you can see the :uri "/favicon.ico" in the second get :D

    ReplyDelete
  7. I echo the positive comments also - thanks for a good intro tutorial. And the "watch -d -n 1 curl -sv http://localhost:8080/" command is a nice trick - learned something new there.

    ReplyDelete

Followers