How can I reduce the duplication in the Clojure code below?

161 views Asked by At

I have the following Clojure code with a render function which renders a html page using enlive-html. Depending on the selected language a different html template is used.

As you can see, there is a lot of code duplication and I would like to remove it.

I was thinking of writing some macros but, if I understand correctly, the language (i.e. lang parameter) is not available at macro execution time because it is provided in the request and that is at execution time and not at compilation time.

I also tried to modify enlive in order to add i18n support at some later point but my Clojure skills are not there yet.

So the questions are:

How can I remove the code duplication in the code below?

Is enlive-html the way to go or should I use another library? Is there a library similar to enlive with support for i18n?

Thanks!

See the code here:

(ns myapp.core
  (:require [net.cgrand.enlive-html :as e))

(deftemplate not-found-en "en/404.html"
  [{path :path}]
  [:#path] (e/content path))

(deftemplate not-found-fr "fr/404.html"
  [{path :path}]
  [:#path] (e/content path))


(defn getTemplate [page lang]
  (case lang
      :en (case page
                :page/not-found not-found-en)
      :fr (case page
                :page/not-found not-found-fr)))

(defn render [lang [page params]]
  (apply (getTemplate page lang) params))
2

There are 2 answers

5
amalloy On BEST ANSWER

On the one hand, it is not too hard to write a macro that will generate the exact code you have here for an arbitrary set of languages. On the other hand, there is probably a better approach than using deftemplate - things that are defd are things you expect to refer to by name in the source code, whereas you just want this thing created and used automatically. But I'm not familiar with the enlive API so I can't say what you should do instead.

If you decide to stick with the macro instead, you could write something like:

(defmacro def-language-404s [languages]
  `(do
     ~@(for [lang languages]
         `(deftemplate ~(symbol (str "not-found-" lang)) ~(str lang "/404.html")
            [{path# :path}]
            [:#path] (e/content path#)))
     (defn get-template [page# lang#]
       (case page#
         :page/not-found (case lang#
                           ~@(for [lang languages
                                   clause [(keyword lang)
                                           (symbol (str "not-found-" lang))]]
                               clause))))))

user> (macroexpand-1 '(def-language-404s [en fr]))
(do
  (deftemplate not-found-en "en/404.html"
    [{path__2275__auto__ :path}]
    [:#path] (content path__2275__auto__))
  (deftemplate not-found-fr "fr/404.html"
    [{path__2275__auto__ :path}]
    [:#path] (content path__2275__auto__))
  (defn get-template [page__2276__auto__ lang__2277__auto__]
    (case page__2276__auto__
      :page/not-found (case lang__2277__auto__
                        :en not-found-en
                        :fr not-found-fr))))
0
vidi On

After quite a bit of Macro-Fu I got to a result that I'm happy with. With some help from a few nice stackoverflowers I wrote the following macros on top of enlive:

(ns hello-enlive
  (:require [net.cgrand.enlive-html :refer [deftemplate]]))

(defn- template-name [lang page] (symbol (str "-template-" (name page) "-" (name lang) "__")))
(defn- html-file [lang page] (str (name lang) "/" (name page) ".html"))
(defn- page-fun-name [page] (symbol (str "-page" (name page))))

(defmacro def-page [app languages [page & forms]]
  `(do
     ~@(for [lang languages]
         `(deftemplate ~(template-name lang page) ~(html-file lang page)
            ~@forms))

      (defn ~(page-fun-name page) [lang#]
         (case lang#
           ~@(for [lang languages
                   clause [(keyword lang) (template-name lang page)]]
               clause)))

      (def ^:dynamic ~app
        (assoc ~app ~page ~(page-fun-name page)))
      ))

(defmacro def-app [app-name languages pages]
  (let [app (gensym "app__")]
    `(do
       (def ~(vary-meta app merge {:dynamic true}) {})

       ~@(for [page# pages]
           `(def-page ~app ~languages ~page#))

       (defn ~app-name [lang# [page# params#]]
         (apply (apply (get ~app page#) [lang#]) params#)))))

...which are then used like this:

The html templates are stored in a tree like this

html/fr/not-found.html
html/fr/index.html
html/en/not-found.html
html/en/index.html
...

...and the rendering logic looks like this:

(def-app my-app [:en :it :fr :de]
  [ [:page/index [] ]

    ;... put your rendering here

    [:page/not-found [{path :path}]
      [:#path] (content path)]])

...and the usage look like this:

...
(render lang [:page/index {}])
(render lang [:page/not-found {:path path}])
...

The result, although it can probably be improved, I think is pretty nice, without duplication and boilerplate code.