Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating Compojure routes from a list

I've just been playing with Compojure recently, and I've got a small basic webapp. For my HTML templates I'm using Enlive, and I've got a namespace that holds all the simple, static pages. The defroute call for these pages looks like this:

(defroutes public-routes
  (GET "/" []
    (info/index-template))
  (GET "/about" []
    (info/about-template))
  (GET "/contact" []
    (info/contact-template)))

I've actually got a few more than that, but that should give the idea of what I'm doing.

Now, I thought, that's really just bunch of repetition on my part, so I thought I would try the following:

(defroutes info-routes
  (map #(GET (str "/" %) [] (ns-resolve 'webapp.pages.info
                                        (symbol (str % "-template"))))
       '("about" "contact")))

Of course, this doesn't work, as the map is returning a lazy sequence and not a body (?) of functions. Does someone know what I need to do to get this idea to work?

Or should I be using a completely different approach to cut down on repeating myself?

like image 265
Mike Avatar asked Apr 28 '11 00:04

Mike


2 Answers

You can always use the routes function which is used by defroutes:

(defroutes info-routes
  (apply routes
    (map #(GET (str "/" %) [] 
               (ns-resolve 'webapp.pages.info
                           (symbol (str % "-template"))))
         '("about" "contact"))))

But that's still quite boring, let's spice it up! ;-)

(defn templates-for [& nss]
  (->> nss
       (map ns-publics)
       (apply concat)
       (filter #(->> % first str
                     (re-seq #"-template$")))
       (map second)))

(defn template-uri [template]
  (->> template meta :name name
       (re-seq  #"(.*)-template$")
       first second (str "/")))

(defn template->route [template]
  (GET (template-uri template) [] template))

(defroutes public-routes
  (GET "/" [] "foo")
  (apply routes (map template->route
                     (templates-for 'webapp.pages.info))))

With this code, the templates-for function will look for any functions finishing with "-template" in the namespaces given and write the appropriate route with them. Look how I'm not using any macro, but plenty of composition.

like image 58
Nicolas Buduroi Avatar answered Sep 28 '22 03:09

Nicolas Buduroi


defroutes is a macro so unfortunately you wont be able to pass it to a function like map. You would need to write a macro that expanded into a call to defroutes. or look at the functions that it expands into and call them directly.

It wont work to create a list of routes within the call to defroutes like this

(defroutes public-routes
  (make-list-of-routes)

would expand into a list of routes:

(defroutes public-routes
  ( (GET "/" [] (info/index-template)) 
    (GET "/about" [] (info/about-template))
    (GET "/contact" [] (info/contact-template))) )

if defroutes where a normal function you would solve this with apply

(apply defroutes (conj 'public-routes (make-list-of-routes)))

because defroutes is a macro it is completely finished before apply could run and the results would not make a lot of sense. You really can't compose macroes as functions. macroes are not first class citizens in clojure (or any lisp i know of) When some Clojurians (not usually me) say "Macroes are evil" they often think about situations like this where you run into the fact that something is a macro when you try to compose it and can't.

the solution is to not use the defroutes macro and call the routes function directly.

like image 36
Arthur Ulfeldt Avatar answered Sep 28 '22 03:09

Arthur Ulfeldt