Monday, October 18, 2010

Lesson 9 - Multimethods and routing for our server

In Lesson 6 we saw how to parse a HTTP Request. In Lesson 7 we learned to send a proper response. For our server to really do something it needs to handle varied requests. So in this lesson we will learn to respond to requests for the index page ("/"), an about page ("/about"), a contact page ("/contact") and send a Not Found error for any other request.

To achieve the above we need to route the requests to the appropriate handler functions or otherwise called controllers. In traditional object oriented programming you would have created a base "Controller" class and created extensions to handle each type of request. In a functional programming language like Clojure we can achieve the same results using multimethods.

First let us write a route map for our program.

(def routes {
  "/" "index",
  "/about" "about",
  "/contact" "contact"
})
Here we define a map called routes. Each key in the map is the request path we want to handle, which is mapped to a corresponding string.

Next we define a multimethod with defmulti and name it controller.

(defmulti controller
  (fn [request]
    (routes (request :path))))
defmulti takes a function as its argument and based on the value returned it will call the appropriate function. In our case we pass it an anonymous function which takes a request object as its argument. The request object is the same parsed request we saw in Lesson 6. First we extract the path from the request (request :path). Then using the path as key in our routes map we return the corresponding string for the path.

Now how will it call the appropriate function? Here is our function to handle the index page.

(defmethod controller "index" [request]
  (send-response
        (assoc html-response :body "This is the index page")))
We have to define each of our required functions with defmethod. It takes the same name as the defmulti, but the next argument after the name is the dispatch-val. For this particular function to be called the function in defmulti must return a value equal to this dispatch-val. In our case the dispatch val is the string "index" which corresponds to our "/" path. The rest of the definition is the same as any other function you define.

Here are our other handler functions.
(defmethod controller "about" [request]
  (send-response
        (assoc html-response :body "This is the about page")))

(defmethod controller "contact" [request]
  (send-response
        (assoc html-response :body "This is the contact page")))
  
(defmethod controller :default [request]
  (println "HTTP/1.0 404 Not Found"))
Notice the last one. A dispatch-val of :default indicates that this is the default function in case no suitable function was found.

Here is the complete source code.
(use 'clojure.contrib.server-socket)
(use '[clojure.string :only (join split)])
(import  '(java.io BufferedReader InputStreamReader PrintWriter))

(def routes {
  "/" "index",
  "/about" "about",
  "/contact" "contact"
})

(defn parse-request []
  (loop
    [ result
        (zipmap
          [:method :path :protocol]
          (split (read-line) #"\s"))
      line (read-line)]
    (if (empty? line)
      result
      (recur
        (assoc
          result
          (keyword (first (split line #":\s+")))
          (last (split line #":\s+")))
        (read-line)))))
         
(def html-response
  { :status-line "HTTP/1.0 200 OK",
    :headers {:Content-Type "text/html"}})
            
(defn send-response [response]
  (let [headers (assoc (response :headers)
                  :Content-Length (count (response :body)))]
    (println (response :status-line))
    (println
      (join
        (for [key (keys headers) :let [value (headers key)]]
          (format "%s: %s\n" (name key) value))))
    (print (response :body))))

(defmulti controller
  (fn [request]
    (routes (request :path))))

(defmethod controller "index" [request]
  (send-response
        (assoc html-response :body "This is the index page")))

(defmethod controller "about" [request]
  (send-response
        (assoc html-response :body "This is the about page")))

(defmethod controller "contact" [request]
  (send-response
        (assoc html-response :body "This is the contact page")))
  
(defmethod controller :default [request]
  (println "HTTP/1.0 404 Not Found"))

(create-server
  8080
  (fn [in out]
    (binding
      [ *in* (BufferedReader. (InputStreamReader. in))
        *out* (PrintWriter. out)]
      (controller (parse-request))
      (flush))))



No comments:

Post a Comment