Ring

deconstructing the functional web
Norman Richards
orb@nostacktrace.com
Response Map

Request Map

HTTP → {} → fn → {} → HTTP
Handler FN
ring-jetty-adaptor

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

(.stop server)
 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 :(
ring.util.servlet/update-servlet-response

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

A few response helpers

(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
""})
(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"))
Building up a response

(defn handler [req]
(-> (response/response "")
(response/status 302)
(response/header "Location" "http://www.google.com")))
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
Abstracting the reloading

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

(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]
Smarter reloading surrounding the wrapped handler
      (doseq [ns-sym (modified-namespaces)]
        (require ns-sym :reload))
      (handler request))))
ring.server.standalone/add-middleware

This is what “lein ring server” does

(defn- add-middleware [handler options]
  (-> handler
      (add-auto-refresh options)
      (add-auto-reload options)
      (add-stacktraces options)))
Middleware stacks
(middleware1 (middleware2 (middleware3 handler)))
(defn ring-stack [handler]
Our custom “ring” middleware stack
(-> handler
(wrap-reload)
(wrap-stacktrace)))
 
(defonce server-atom (atom nil))
And some custom sever code
 
(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)))
ring.middleware.*

•
•
•
•
•
•
•
•
•
•
•
•

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

Lot’s of middleware to choose from
compojure.handler/api
An existing minimal stack for APIs

(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))
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))
Extends the API stack
      (wrap-flash)
      (with-opts wrap-session (:session opts))))
Use it, or make your own

(def handler (-> #'app
compojure.handler/site
ring-stack))
 
noir.util.middleware/app-handler
(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)
A hook to extend the noir stack
        (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)
Lot’s of customization
        (wrap-noir-cookies)
        (wrap-noir-flash)
        (wrap-noir-session
         (update-in session-options [:store] #(or % (memory-store mem)))))))
Routing
Route: (RequestMap → (Option ResponseMap))
Not ring handlers
because they don’t
take a request.

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

Select the page to
show based on URL
Abstract the route dispatch

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

(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)
Much cleaner
(my-route #"/foo"
foo)
(my-route #"/foo/(.*)" foo-n)
(my-route #".*"
#(response/not-found "Wat"))]]
(some #(% req) my-routes)))
The first route that responds wins
Routing fn includes method and path

(defn app4 [req]
(let [my-routes [(GET "/" [] (home))
Some extra macro magic
(GET "/foo" [] (foo))
(GET "/foo/:id" [id] (foo-n id))
(route/not-found "Wat")]]
(some #(% req) my-routes)))
 
Some things never change
compojure.core/*
(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))
a collection of routes

(def app5
(routes
(GET "/" [] (home))
(GET "/foo" [] (foo))
(GET "/foo/:id" [id] (foo-n id))
(route/not-found "Wat")))
(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))
Routing functions nest easily
foo-routes
(route/not-found "Wat")))
(defn foobar-routes [foobar-type]
(routes
(GET "/" [] (str foobar-type " Page"))
(GET "/:id" [id] (str foobar-type "#" id))))
 
(def app7
Not ideal - generates the route fn each call
(routes
(GET "/" [] (home))
(context "/foo" [] (foobar-routes "Foo"))
(context "/bar" [] (foobar-routes "Bar"))
(route/not-found "Wat")))
(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))))))
compojure.response/Renderable
(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 … )

Generate response maps based on types
Norman Richards
orb@nostacktrace.com

Deconstructing the Functional Web with Clojure

  • 1.
    Ring deconstructing the functionalweb Norman Richards orb@nostacktrace.com
  • 2.
    Response Map Request Map HTTP→ {} → fn → {} → HTTP Handler FN
  • 3.
    ring-jetty-adaptor ring handler (def server (ring.adapter.jetty/run-jetty#’handler {:port 8080 :join? false})) ! (.stop server)
  • 4.
     ring.adaptor.jetty/proxy-handler (defn- proxy-handler   "Returns anJetty 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))))))
  • 5.
    ring.util.servlet/build-request-map (defn build-request-map   "Create therequest 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 :(
  • 6.
    ring.util.servlet/update-servlet-response (defn update-servlet-response   "Update theHttpServletResponse 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)))
  • 7.
  • 8.
    (defn handler-nil [req] {:bodynil}) response body
  • 9.
    ring.util.servlet/set-body (defn- set-body   "Update aHttpServletResponse 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)))))
  • 10.
  • 11.
    (defn handler-file [req] {:body(clojure.java.io/file "info.txt")})
  • 12.
    (defn handler-status [req] {:status402 :headers {"Location" "bitcoin:1G9TyAaKrfJn7q4Vrr15DscLXFSRPxBFaH?amount=.001"}}) Handlers can return status code and headers
  • 13.
    ring.util.response/* A few responsehelpers (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 ""})
  • 14.
    (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"))
  • 15.
    Building up aresponse (defn handler [req] (-> (response/response "") (response/status 302) (response/header "Location" "http://www.google.com")))
  • 16.
  • 17.
    (defn handler-reload1 [req] (response/response(reload-me/some-work))) A function we’d like to be reloaded if it changes
  • 18.
    (defn handler-reload2 [req] (require'ringtest.reload-me :reload) (handler-reload1 req)) The original handler is wrapped
  • 19.
    Abstracting the reloading (defnwrap-reload [other-handler] (fn [req] (require 'ringtest.reload-me :reload) (other-handler req)))   (def handler-reload3 (wrap-reload #'handler-reload1))
  • 20.
    ring.middleware.reload/wrap-reload (defn wrap-reload   "Reload namespacesof 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] Smarter reloading surrounding the wrapped handler       (doseq [ns-sym (modified-namespaces)]         (require ns-sym :reload))       (handler request))))
  • 21.
    ring.server.standalone/add-middleware This is what“lein ring server” does (defn- add-middleware [handler options]   (-> handler       (add-auto-refresh options)       (add-auto-reload options)       (add-stacktraces options)))
  • 22.
  • 23.
    (defn ring-stack [handler] Ourcustom “ring” middleware stack (-> handler (wrap-reload) (wrap-stacktrace)))   (defonce server-atom (atom nil)) And some custom sever code   (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)))
  • 24.
  • 25.
    compojure.handler/api An existing minimalstack for APIs (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))
  • 26.
    compojure.handler/site (defn site   "Create ahandler 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)) Extends the API stack       (wrap-flash)       (with-opts wrap-session (:session opts))))
  • 27.
    Use it, ormake your own (def handler (-> #'app compojure.handler/site ring-stack))  
  • 28.
    noir.util.middleware/app-handler (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) A hook to extend the noir stack         (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) Lot’s of customization         (wrap-noir-cookies)         (wrap-noir-flash)         (wrap-noir-session          (update-in session-options [:store] #(or % (memory-store mem)))))))
  • 29.
    Routing Route: (RequestMap →(Option ResponseMap))
  • 30.
    Not ring handlers becausethey don’t take a request. (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"))) Select the page to show based on URL
  • 31.
    Abstract the routedispatch (defn route-to [handler] (fn [match] (if (string? match) (handler) (apply handler (rest match)))))   (defn app2 [req] (condp re-matches (:uri req) #"/" Cleaner, but still awkward :>> (route-to home)   #"/foo" :>> (route-to foo)   #"/foo/(.*)" :>> (route-to foo-n)   (response/not-found "Wat")))
  • 32.
    Include the pattern (defnmy-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) Much cleaner (my-route #"/foo" foo) (my-route #"/foo/(.*)" foo-n) (my-route #".*" #(response/not-found "Wat"))]] (some #(% req) my-routes))) The first route that responds wins
  • 33.
    Routing fn includesmethod and path (defn app4 [req] (let [my-routes [(GET "/" [] (home)) Some extra macro magic (GET "/foo" [] (foo)) (GET "/foo/:id" [id] (foo-n id)) (route/not-found "Wat")]] (some #(% req) my-routes)))   Some things never change
  • 34.
    compojure.core/* (defn make-route   "Returns afunction 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))
  • 35.
    a collection ofroutes (def app5 (routes (GET "/" [] (home)) (GET "/foo" [] (foo)) (GET "/foo/:id" [id] (foo-n id)) (route/not-found "Wat")))
  • 36.
    (defn routing   "Apply alist 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))
  • 37.
    (def foo-routes (routes (GET "/foo"[] (foo)) (GET "/foo/:id" [id] (foo-n id))))   (def app6 (routes (GET "/" [] (home)) Routing functions nest easily foo-routes (route/not-found "Wat")))
  • 38.
    (defn foobar-routes [foobar-type] (routes (GET"/" [] (str foobar-type " Page")) (GET "/:id" [id] (str foobar-type "#" id))))   (def app7 Not ideal - generates the route fn each call (routes (GET "/" [] (home)) (context "/foo" [] (foobar-routes "Foo")) (context "/bar" [] (foobar-routes "Bar")) (route/not-found "Wat")))
  • 39.
    (defmacro context   "Give allroutes 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))))))
  • 40.
    compojure.response/Renderable (defprotocol Renderable   (render [thisrequest]     "Render the object into a form suitable for the given request map.")) ! (extend-protocol Renderable   nil …   String …   APersistentMap …   IFn …   IDeref …   File …   ISeq …   InputStream …   URL … ) Generate response maps based on types
  • 41.