Joshua Mendoza
Joshua Mendoza

Reputation: 41

In Clojure, is there an idiomatic way of destructuring a map in a macro definition?

I've been using noir in a web project and I came up to the point of restricting access to users, depending on their access level (and sublevel) to all possible routes defined by a defpage macro. So originally I had

(defpage [:post "/mysite"] {:as input}
  (if-not (has-reqd-user-level? :levelX :sublevelY "/grantedRoute")
    (noir.response/redirect "/insufficientRights")
    ...))

And then I thought this would get rid of boilerplate code:

(defmacro defpage-with-user-level [level sublevel granted-route route data expr]
  `(defpage ~route ~data
     (if-not (has-reqd-user-level? ~level ~sublevel ~granted-route)
       (noir.response/redirect "/insufficientRights")
       ~expr)))

Finally, we use it as follows:

(defpage-with-user-level :levelX :sublevelY "/grantedRoute"
  [:post "/mysite"] {:as input}
  (html
    [:body [:h1 (str "Hello " (:name input) "!")]]))

But as mentioned in this post made by Rich Hickey, https://groups.google.com/forum/#!msg/clojure/4II-HKr_Pu0/2IcKit99cagJ , it feels a little bit awkward because of positional binding, which is not idiomatic when there already exist maps.

However, I've been looking for some examples or discussions regarding the use of destructuring bindings in macros, and sadly, I hadn't found any clear use of them, because of its unevaluated expressions being passed through all along.

So, the following solution came to my mind:

(defmacro defpage-with-user-level [dts expr]
  `(defpage (:route ~dts) (:data ~dts)
     (if-not (has-reqd-user-level? (:level ~dts) (:sublevel ~dts) (:granted-route ~dts))
       (noir.response/redirect "/insufficientRights")
       ~expr)))

But now, it's not clear how to pass the data map that maps locals from :get and :post into a local as in the examples above.

Am I doing right leaving my first attempt untampered or do I really need to use the second approach? I hope not. Is there any other option? Please, let me know.

Upvotes: 3

Views: 1090

Answers (1)

Ankur
Ankur

Reputation: 33637

Your first solution is fine. What Rich was talking about was using plain old maps to passing data around rather then creating new types/classes for each type of data. Ex: you can represnt a user information using a simple map rather than creating a class to represent user data.

As far as your second attempt is concerned, you can use map de-structuring in macro as:

(defmacro defpage-with-user-level [{:keys [route data level sublevel granted-route]} expr]
  `(defpage ~route ~data
     (if-not (has-reqd-user-level? ~level ~sublevel ~granted-route)
       (noir.response/redirect "/insufficientRights")
       ~expr)))

(defpage-with-user-level {:level :levelX 
                          :sublevel :sublevelY 
                          :granted-route "/grantedRoute"
                          :route [:post "/mysite"] 
                          :data {:as input}}
  (html
    [:body [:h1 (str "Hello " (:name input) "!")]]))

Upvotes: 4

Related Questions