41
Norman Richards [email protected] deconstructing the functional web Ring

Deconstructing the Functional Web with Clojure

Embed Size (px)

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

Page 1: Deconstructing the Functional Web with Clojure

Norman Richards [email protected]

deconstructing the functional webRing

Page 2: Deconstructing the Functional Web with Clojure

HTTP → {} → fn → {} → HTTP

Request Map Response Map

Handler FN

Page 3: Deconstructing the Functional Web with Clojure

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

ring-jetty-adaptor

ring handler

Page 4: Deconstructing the Functional Web with Clojure

 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))))))

Page 5: Deconstructing the Functional Web with Clojure

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 :(

Page 6: Deconstructing the Functional Web with Clojure

(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

Page 7: Deconstructing the Functional Web with Clojure

Ring Handlers

Handler: (RequestMap → ResponseMap)

Page 8: Deconstructing the Functional Web with Clojure

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

response body

Page 9: Deconstructing the Functional Web with Clojure

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)))))

Page 10: Deconstructing the Functional Web with Clojure

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

Page 11: Deconstructing the Functional Web with Clojure

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

Page 12: Deconstructing the Functional Web with Clojure

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

Handlers can return status code and headers

Page 13: Deconstructing the Functional Web with Clojure

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

Page 14: Deconstructing the Functional Web with Clojure

(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"))

Page 15: Deconstructing the Functional Web with Clojure

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

Building up a response

Page 16: Deconstructing the Functional Web with Clojure

Wrapping requests (middleware)

Middleware: (Handler → Handler)

Page 17: Deconstructing the Functional Web with Clojure

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

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

Page 18: Deconstructing the Functional Web with Clojure

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

The original handler is wrapped

Page 19: Deconstructing the Functional Web with Clojure

(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

Page 20: Deconstructing the Functional Web with Clojure

(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

Page 21: Deconstructing the Functional Web with Clojure

(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

Page 22: Deconstructing the Functional Web with Clojure

Middleware stacks

(middleware1 (middleware2 (middleware3 handler)))

Page 23: Deconstructing the Functional Web with Clojure

(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

Page 25: Deconstructing the Functional Web with Clojure

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

Page 26: Deconstructing the Functional Web with Clojure

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

Page 27: Deconstructing the Functional Web with Clojure

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

Use it, or make your own

Page 28: Deconstructing the Functional Web with Clojure

(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

Page 29: Deconstructing the Functional Web with Clojure

Routing

Route: (RequestMap → (Option ResponseMap))

Page 30: Deconstructing the Functional Web with Clojure

(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

Page 31: Deconstructing the Functional Web with Clojure

(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

Page 32: Deconstructing the Functional Web with Clojure

(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

Page 33: Deconstructing the Functional Web with Clojure

(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

Page 34: Deconstructing the Functional Web with Clojure

(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/*

Page 35: Deconstructing the Functional Web with Clojure

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

a collection of routes

Page 36: Deconstructing the Functional Web with Clojure

(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))

Page 37: Deconstructing the Functional Web with Clojure

(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

Page 38: Deconstructing the Functional Web with Clojure

(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

Page 39: Deconstructing the Functional Web with Clojure

(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))))))

Page 40: Deconstructing the Functional Web with Clojure

(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