I'm working on a server-rendered website while learning Clojure, Ring, Compojure etc; could somebody help me understand something about handlers & middleware?
I have a function main-layout whose purpose is to wrap its inputs with the site's basic HTML (using https://github.com/escherize/huff):
(defn main-layout [req & content]
(str
(huff/html
[:body
(when (logged-in? req) [:p "logged in."])
[:nav>ul
[:li>a {:href "/"} "home"]
[:li>a {:href "/log-in"} "log in"]]
[:main content]])))
Wrapping all my handlers with this works fine, but I had hoped to reduce the tedium a little bit by creating a middleware that would handle this wrapping business for me—however, it seems like Ring doesn't like handlers returning vectors: No implementation of method: :render of protocol: #'compojure.response/Renderable found for class: clojure.lang.PersistentVector.
This is the wrapper I've put together:
(defn wrap-layout [handler]
(fn [req]
(let [response (handler req)]
(assoc response :body (main-layout req (:body response))))))
So either this just won't work the way I thought, or I'm doing something really wrong. Is the usual way to share a layout just to wrap all the handlers manually, or is there a better way?
(defn home-page
"The way that works"
[req]
(main-layout
req
[:h2 "home page"]))
(defn home-page-concisely
"The way I want it"
[_req]
[:h2 "home-page"])Is your repo up somewhere public where we can see the whole thing? Hard to tell what to suggest from what you've show here...
Ring handlers must return a Ring response, so you'll need something like:
(defn home-page-concisely
[_req]
{:status 200
:body [:h2 "home-page"]})So this isn't Ring per se, but Compojure.
Compojure uses the compojure.response/render function to turn a value, like a string, into a full response. Your middleware is applied after Compojure tries to render the return value into a response, so Compojure errors before it gets to your middleware.
There are two solutions. The first is to always return a map with the vector as the :body, as @jkrasnay suggests. This might be good practice, as it means you're always explicit about which response code and headers you're using.
Alternatively, you can extend the compojure.response/Renderable protocol treat vectors as response bodies:
(extend-protocol compojure.response/Renderable
clojure.lang.APersistentVector
(render [v _]
{:status 200
:headers {"Content-Type" "text/html; charset=utf-8"}
:body (str "\n" (huff/html v))}))
With this protocol extension, any bare vector will be converted into a string of HTML. This allows you to return bare vectors:
(GET "/" []
[:html [:head [:title "Hello World"]] [:body "Hello World"]])
While also allowing you to return maps when you need custom headers or response codes.
(GET "/404" []
{:status 404
:body [...]})That's very interesting! I wasn't sure if it was Compojure or Ring doing some magic with those plain strings. I'm going to play around with extending the protocol like you suggest, thank you