Upload
norman-richards
View
1.284
Download
3
Tags:
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
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
• wrap-content-type • wrap-cookies • wrap-file-info • wrap-flash • wrap-head • wrap-keyword-params • wrap-multi-part-params • wrap-nested-params • wrap-not-modified • wrap-params • wrap-resource • wrap-session
ring.middleware.*
Lot’s of middleware to choose from
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 [email protected]