14 Nov 2012

defhandler, a clojure macro aid define ring handler

Currently, I am working at AVOS Systems’s China team. I am in a group responsible for building meiweisq, The Delicious web service, but for Chinese users, and using Clojure!

We are using Ring and Compojure. Ring’s request/response abstraction is great, Compojure’s defroutes family functions get its job done perfectly. We are just happy using it.

A ring handler is just a plain Clojure function, passing a request map, return a response map:

(defn handler [req]
  ;; return a map of keys [:status :body :headers]
  )

There are some boilerplate code, in this pattern:

;;; get the params
(let [{:keys [id tag]} (:params req)
      limit (min 30 (to-int (or (:limit (:params req)) 15)))
      offset (to-int (or (:offset (:params req)) 0))]
  ;; do work with these params
  )

They are repeated in the source code. Repeats are bad. Macro comes to the rescue:

(defmacro defhandler [handler bindings & body]
  (let [req (bindings 0)
        bindings (rest bindings)
        ks (map (fn [s] (keyword (name s))) bindings)
        vals (map (fn [k]
                    ;; limit and offset are special, set the default value, guard large
                    ;; limit, convert them to int
                    (cond (= :limit k) `(min 30 (to-int (or (~k (:params ~req)) 15)))
                          (= :offset k) `(to-int (or (~k (:params ~req)) 0))
                          ;; more pattens here, something like
                          ;; (= :uid k) code to get user id
                          :else `(~k (:params ~req)))) ks)]
    `(defn ~handler [~req]
       (let [~@(interleave bindings vals)]
         ~@body))))

example usage

(defhandler list-feeds [req id limit offset]
  {:status 200
   :body (str "the subscription id: " id
              "limit:" limit
              "offset:" offset)})

Macroexpand to

(defn list-feeds [req]
  (let [id (:id (:params req))
        limit (min 30 (to-int (or (:limit (:params req)) 15)))
        offset (to-int (or (:offset (:params req)) 0))]
    {:status 200
     :body (str "the subscription id: " id
                "limit:" limit
                "offset:" offset)}))

Boilerplate killed. I am happy now.

By the way, to-int source code:

(defn to-int [s] (cond
                  (string? s) (Integer/parseInt s)
                  (instance? Integer s) s
                  (instance? Long s) (.intValue ^Long s)
                  :else 0))
blog comments powered by Disqus