Transcript
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


Recommended