Deconstructing the Functional Web with Clojure

Preview:

DESCRIPTION

Programming for the web in Clojure isn't hard, but with layers of abstraction you can easily lose track of what is going on. In this talk, we'll dig deep into Ring, the request/response library that most Clojure web programming is based on. We'll see exactly what a Ring handler is and look at the middleware abstraction in depth. We'll then take this knowledge and deconstruct the Compojure routing framework to understand precisely how your web application responds to request. At the end of the talk you should thoroughly understand everything that happens in the request/response stack and be able to customize your web application stack with confidence. Updated for Houston Clojure Meetup 2/28/14

Citation preview

Norman Richards orb@nostacktrace.com

deconstructing the functional webRing

HTTP → {} → fn → {} → HTTP

Request Map Response Map

Handler FN

(def server (ring.adapter.jetty/run-jetty #’handler {:port 8080 :join? false})) !(.stop server)

ring-jetty-adaptor

ring handler

 ring.adaptor.jetty/proxy-handler

(defn- proxy-handler   "Returns an Jetty Handler implementation for the given Ring handler."   [handler]   (proxy [AbstractHandler] []     (handle [_ ^Request base-request request response]       (let [request-map (servlet/build-request-map request)             response-map (handler request-map)]         (when response-map           (servlet/update-servlet-response response response-map)           (.setHandled base-request true))))))

ring.util.servlet/build-request-map

(defn build-request-map   "Create the request map from the HttpServletRequest object."   [^HttpServletRequest request]   {:server-port (.getServerPort request)    :server-name (.getServerName request)    :remote-addr (.getRemoteAddr request)    :uri (.getRequestURI request)    :query-string (.getQueryString request)    :scheme (keyword (.getScheme request))    :request-method (keyword (.toLowerCase (.getMethod request)))    :headers (get-headers request)    :content-type (.getContentType request)    :content-length (get-content-length request)    :character-encoding (.getCharacterEncoding request)    :ssl-client-cert (get-client-cert request)    :body (.getInputStream request)})

not functional :(

(defn update-servlet-response   "Update the HttpServletResponse using a response map."   [^HttpServletResponse response, {:keys [status headers body]}]   (when-not response     (throw (Exception. "Null response given.")))   (when status     (set-status response status))   (doto response     (set-headers headers)     (set-body body)))

ring.util.servlet/update-servlet-response

Ring Handlers

Handler: (RequestMap → ResponseMap)

(defn handler-nil [req] {:body nil})

response body

ring.util.servlet/set-body

(defn- set-body   "Update a HttpServletResponse body with a String, ISeq, File or InputStream."   [^HttpServletResponse response, body]   (cond     (string? body)       (with-open [writer (.getWriter response)]         (.print writer body))     (seq? body)   ;; …     (instance? File body)   ;; …     (nil? body)       nil     :else       (throw (Exception. ^String (format "Unrecognized body: %s" body)))))

(defn handler-string [req] {:body "hello world”})

(defn handler-file [req] {:body (clojure.java.io/file "info.txt")})

(defn handler-status [req] {:status 402 :headers {"Location" "bitcoin:1G9TyAaKrfJn7q4Vrr15DscLXFSRPxBFaH?amount=.001"}})

Handlers can return status code and headers

ring.util.response/*(defn response   "Returns a skeletal Ring response with the given body, status of 200, and no headers."   [body]   {:status 200    :headers {}    :body body}) !(defn not-found   "Returns a 404 'not found' response."   [body]   {:status 404    :headers {}    :body body}) !(defn redirect   "Returns a Ring response for an HTTP 302 redirect."   [url]   {:status 302    :headers {"Location" url}    :body ""})

A few response helpers

(defn handler [req] (response/response "Hello, world!”)) !(defn handler [req] (response/redirect "http://lmgtfy.com/?q=http+redirect")) !(defn handler [req] (response/resource-response "hello.txt"))

(defn handler [req] (-> (response/response "") (response/status 302) (response/header "Location" "http://www.google.com")))

Building up a response

Wrapping requests (middleware)

Middleware: (Handler → Handler)

(defn handler-reload1 [req] (response/response (reload-me/some-work)))

A function we’d like to be reloaded if it changes

(defn handler-reload2 [req] (require 'ringtest.reload-me :reload) (handler-reload1 req))

The original handler is wrapped

(defn wrap-reload [other-handler] (fn [req] (require 'ringtest.reload-me :reload) (other-handler req)))   (def handler-reload3 (wrap-reload #'handler-reload1))

Abstracting the reloading

(defn wrap-reload   "Reload namespaces of modified files before the request is passed to the supplied handler. ! Takes the following options: :dirs - A list of directories that contain the source files. Defaults to [\"src\"]."   [handler & [options]]   (let [source-dirs (:dirs options ["src"])         modified-namespaces (ns-tracker source-dirs)]     (fn [request]       (doseq [ns-sym (modified-namespaces)]         (require ns-sym :reload))       (handler request))))

ring.middleware.reload/wrap-reload

Smarter reloading surrounding the wrapped handler

(defn- add-middleware [handler options]   (-> handler       (add-auto-refresh options)       (add-auto-reload options)       (add-stacktraces options)))

ring.server.standalone/add-middleware

This is what “lein ring server” does

Middleware stacks

(middleware1 (middleware2 (middleware3 handler)))

(defn ring-stack [handler] (-> handler (wrap-reload) (wrap-stacktrace)))   (defonce server-atom (atom nil))   (defn start [handler] (swap! server-atom (fn [server] (when server (.stop server)) (jetty/run-jetty handler {:port 8080 :join? false}))))   (defn stop [] (swap! server-atom (fn [server] (when server (.stop server)) nil)))

Our custom “ring” middleware stack

And some custom sever code

compojure.handler/api

(defn api   "Create a handler suitable for a web API. This adds the following middleware to your routes: - wrap-params - wrap-nested-params - wrap-keyword-params"   [routes]   (-> routes       wrap-keyword-params       wrap-nested-params       wrap-params))

An existing minimal stack for APIs

compojure.handler/site

(defn site   "Create a handler suitable for a standard website. This adds the following middleware to your routes: - wrap-session - wrap-flash - wrap-cookies - wrap-multipart-params - wrap-params - wrap-nested-params - wrap-keyword-params ! A map of options may also be provided. These keys are provided: :session - a map of session middleware options :multipart - a map of multipart-params middleware options"   [routes & [opts]]   (-> (api routes)       (with-opts wrap-multipart-params (:multipart opts))       (wrap-flash)       (with-opts wrap-session (:session opts))))

Extends the API stack

(def handler (-> #'app compojure.handler/site ring-stack))  

Use it, or make your own

(defn app-handler [app-routes & {:keys [session-options store multipart middleware access-rules formats]}]   (letfn [(wrap-middleware-format [handler]             (if formats (wrap-restful-format handler :formats formats) handler))]     (-> (apply routes app-routes)         (wrap-middleware middleware)         (wrap-request-map)         (api)         (wrap-base-url)         (wrap-middleware-format)         (with-opts wrap-multipart-params multipart)         (wrap-access-rules access-rules)         (wrap-noir-validation)         (wrap-noir-cookies)         (wrap-noir-flash)         (wrap-noir-session          (update-in session-options [:store] #(or % (memory-store mem)))))))

noir.util.middleware/app-handler

Lot’s of customization

A hook to extend the noir stack

Routing

Route: (RequestMap → (Option ResponseMap))

(defn home [] (response/response "Home Page"))   (defn foo [] (response/response "Foo Page"))   (defn foo-n [n] (response/response (str "This is Foo#" n)))     (defn app1 [req] (condp re-matches (:uri req) #"/" (home) #"/foo" (foo) #"/foo/(.*)" :>> #(foo-n (second %)) (response/not-found "Wat")))

Not ring handlers because they don’t

take a request.

Select the page to show based on URL

(defn route-to [handler] (fn [match] (if (string? match) (handler) (apply handler (rest match)))))   (defn app2 [req] (condp re-matches (:uri req) #"/" :>> (route-to home)   #"/foo" :>> (route-to foo)   #"/foo/(.*)" :>> (route-to foo-n)   (response/not-found "Wat")))

Abstract the route dispatch

Cleaner, but still awkward

(defn my-route [pattern page-fn] (fn [req] (if-let [match (re-matches pattern (:uri req))] ((route-to handler) page-fn))))   (defn app3 [req] (let [my-routes [(my-route #"/" home) (my-route #"/foo" foo) (my-route #"/foo/(.*)" foo-n) (my-route #".*" #(response/not-found "Wat"))]] (some #(% req) my-routes)))

Include the pattern

Much cleaner

The first route that responds wins

(defn app4 [req] (let [my-routes [(GET "/" [] (home)) (GET "/foo" [] (foo)) (GET "/foo/:id" [id] (foo-n id)) (route/not-found "Wat")]] (some #(% req) my-routes)))  

Routing fn includes method and path

Some things never change

Some extra macro magic

(defn make-route   "Returns a function that will only call the handler if the method and Clout route match the request."   [method route handler]   (if-method method     (if-route route       (fn [request]         (render (handler request) request))))) !(defn- compile-route   "Compile a route in the form (method path & body) into a function."   [method route bindings body]   `(make-route     ~method ~(prepare-route route)     (fn [request#]       (let-request [~bindings request#] ~@body)))) !(defmacro GET "Generate a GET route."   [path args & body]   (compile-route :get path args body))

compojure.core/*

(def app5 (routes (GET "/" [] (home)) (GET "/foo" [] (foo)) (GET "/foo/:id" [id] (foo-n id)) (route/not-found "Wat")))

a collection of routes

(defn routing   "Apply a list of routes to a Ring request map."   [request & handlers]   (some #(% request) handlers)) !(defn routes   "Create a Ring handler by combining several handlers into one."   [& handlers]   #(apply routing % handlers))

(def foo-routes (routes (GET "/foo" [] (foo)) (GET "/foo/:id" [id] (foo-n id))))   (def app6 (routes (GET "/" [] (home)) foo-routes (route/not-found "Wat")))

Routing functions nest easily

(defn foobar-routes [foobar-type] (routes (GET "/" [] (str foobar-type " Page")) (GET "/:id" [id] (str foobar-type "#" id))))   (def app7 (routes (GET "/" [] (home)) (context "/foo" [] (foobar-routes "Foo")) (context "/bar" [] (foobar-routes "Bar")) (route/not-found "Wat")))

Not ideal - generates the route fn each call

(defmacro context   "Give all routes in the form a common path prefix and set of bindings. ! The following example demonstrates defining two routes with a common path prefix ('/user/:id') and a common binding ('id'): ! (context \"/user/:id\" [id] (GET \"/profile\" [] ...) (GET \"/settings\" [] ...))"   [path args & routes]   `(#'if-route ~(context-route path)      (#'wrap-context        (fn [request#]          (let-request [~args request#]            (routing request# ~@routes))))))

(defprotocol Renderable   (render [this request]     "Render the object into a form suitable for the given request map.")) !(extend-protocol Renderable   nil …   String …   APersistentMap …   IFn …   IDeref …   File …   ISeq …   InputStream …   URL … )

compojure.response/Renderable

Generate response maps based on types

Norman Richards orb@nostacktrace.com

Recommended