During our last pairing session with Jacek Laskowski on Librarian there was a brief moment of confusion over Ring handlers. We struggled for a short while trying to figure out what order to put them in and what it really means to have code like:
(def app (-> routes ; sandbar (auth/with-security security-policy log-in) ; compojure helper that includes a few Ring handlers site ; sandbar again session/wrap-stateful-session))
It didn’t take us long to figure it out and the solution turns out to be a very elegant functional flavor of decorator.
It’s easy to dive too deep without proper understanding (and that’s what I admittedly did). Let’s start from the very beginning and see what these bits really mean. For starters, here’s a very basic app in plain Ring that simply returns the entire request
:
(defn my-handler [request] {:body (str request)}) (def app my-handler)
When I hit http://localhost:3000/?my_param=54 in my browser, in return I get:
{:remote-addr "0:0:0:0:0:0:0:1", :scheme :http, :request-method :get, :query-string "my_param=54", :content-type nil, :uri "/", :server-name "localhost", :headers {"cookie" "__utma=111872281.60059650.1328613066.1328726201.1328785442.5; __utmz=111872281.1328613066.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)", "connection" "keep-alive", "accept-encoding" "gzip, deflate", "accept-language" "pl,en-us;q=0.7,en;q=0.3", "accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "user-agent" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) Gecko/20100101 Firefox/11.0", "host" "localhost:3000"}, :content-length nil, :server-port 3000, :character-encoding nil, :body #<Input org.mortbay.jetty.HttpParser$Input@7c04703c>}
Note that my_param
made it to :query-string
, but obviously it’s quite inconvenient at this point and that’s not what we really want to deal with.
What is app
at this point? No magic here, it’s just a very simple and boring function.
Let’s move on and add one of the seemingly magic Ring handlers – ring.middleware.params/wrap-params
:
(def app (wrap-params my-handler))
This time for the same URL I get the same map, with a few new entries:
{:remote-addr "0:0:0:0:0:0:0:1", ; Trimmed for brevity :query-params {"my_param" "54"}, :form-params {}, :query-string "my_param=54", :params {"my_param" "54"}}
I can see that the wrapper added a few new entries: :query-params
, :form-params
and :params
. Great, just like it was supposed to.
Now, what is app
at this point? Just like before, it’s a regular function of request
. So what does wrap-params
really do? Let’s take a peek at (parts of) its source:
(defn wrap-params [handler & [opts]] (fn [request] (let [request (if (:query-params request) request (assoc-query-params request))] (handler request))))
assoc-query-params
is no magic, it simply parses query params and merges it with the request
map.
Now let’s take a close look at the last line and back at wrap-params
signature. Here’s what’s really going on:
wrap-params
takes handler (which is a function ofrequest
) as argument. In our case, it’s the trivial function that returnsrequest
in body.- It then performs some work, in this case rebinding
request
to a map with a few more entries. - Eventually it calls the
handler
that it got as parameter with the augmentedrequest
map.
In other words, wrap-params
takes a handler
function, and returns a function that performs some extra work and invokes the original handler
.
Does it look familiar? Yup, it’s the old good decorator pattern. Do some work, then pass control on to the next handler (which can also be a decorator and delegate it further). In this case, though, it’s astonishingly simple (compared to what it takes in Java).
Now let’s say I want to chain one more handler that relies on the previous one. Let’s say I dislike strings and want to map params by Clojure keywords. There’s a handler for it: ring.middleware.keyword-params/wrap-keyword-params
.
No need to think too long, let’s jump in and use it:
(def app (wrap-keyword-params (wrap-params my-handler)))
… and I get:
{; Trimmed for brevity :params {"my_param" "54"}}
Whoops, that’s not what I expected. wrap-keyword-params
was supposed to create a map with keys as keywords, not strings. Why didn’t it work?
Naive intuition tells me to treat wrappers as function calls. I wrap my-handler
in wrap-params
and pass the result of this invocation to wrap-keyword-params
, right? Wrong!
Take a look at a sample wrapper above (wrap-params
) and think what we were trying to do. What I really created here is a reversed chain like:
- Given a
request
, map its:params
into keywords (wrap-keyword-params
). - Then pass control to the next function in chain,
wrap-params
. It parses query string and adds:params
map torequest
. - Then pass control to
my-handler
which prints the whole thing to browser
Nothing happens in the first step, because :params
does exist at this point – it’s only created by wrap-params
in the second step.
If we reverse it, it works like expected:
(def app (wrap-params (wrap-keyword-params my-handler)))
{; Trimmed for brevity :params {:my_param "54"}}
To recap, a few things to remember from this lesson:
- In functional programming, the decorator pattern is elegantly represented as a higher order function. I find it much easier to grasp than the OO flavor – in Java I would need an interface and 3 implementing classes for the job, greatly limiting (re)usability and readability.
- In case of Ring wrappers, typically we have “before” decorators that perform some preparations before calling the “real” business function. Since they are higher order functions and not direct function calls, they are applied in reversed order. If one depends on the other, the dependent one needs to be on the “inside”.
nice and simple for a newbie to clojure. thanks for the insight.
now, are there any “after” wrappers in ring? is there a convention to tell the difference?
belun,
there are “after” and “around” wrappers, but I don’t think they are easily distinguishable by name etc. The way to go is to think what a handler does and where it fits in the decorator chain.
Wrappers like wrap-params or wrap-keyword-params are clearly useful for the “inner” handlers by doing something to the request map. If something relies on them (e.g. some form processing, like authentication), you need to chain them in the right order.
One example of an “around” wrapper is wrap-flash. If you put something in :flash in a response, next time a request on the same session comes in it will include the same thing in :flash in request.
For example of an “after” wrapper see wrap-content-type. If your response does not include Content-Type, it automatically adds it to the response.