Sunday, July 7, 2013

To clabango or not to clabango

Pedestal's lack of documentation and confusing client/server development model caused me to switch to luminus, which comes bundled with clabango. It's been a while since I've done template based page generation, so maybe this is par for the course. So far clabango has left me a bit underwhelmed. It might prove to be sufficient for my needs, however, so I'm trying to find ways to work with the package.

The first thing that struck me is its utter un-lisp-ness. That seems to be a positive for some. More troubling to me is how clabango seems to encourage breaking data abstractions. I have a protocol Identifier that I would like to render in a page. I have two options when I do so: pull out the value from the record implementing the protocol ahead of time, or access the record's raw fields through the map API. There doesn't appear to be a reasonable way to render through the protocol methods on the page.

;; Define an identifier protocol and a widget-id record
user=> (defprotocol identifier
         (value [this]))
identifier
user=> (defrecord widget-id [id-value]
         identifier
         (value [id]
           (.id-value id)))
user.widget-id
user=> (require 'clabango.parser)
nil
user=> (alias 'parser 'clabango.parser)
nil
;; This is obviously wrong user=> (parser/render "The ID is {{ id }}." {:id (->widget-id "abc")}) "The ID is user.widget-id@caada592."
;; Can't use the protocol abstraction user=> (parser/render "The ID is {{ id.value }}." {:id (->widget-id "abc")}) "The ID is ."
;; Works if we don't use the abstraction,
;; which defeats the purpose of having one at all
user=> (parser/render "The ID is {{ id.id-value }}." {:id (->widget-id "abc")}) "The ID is abc."
;; Works if we pass in the raw value,
;; which doesn't propagate the abstraction through the whole program.
;; Once again it defeats the purpose of the abstraction.
user=> (parser/render "The ID is {{ id-value }}." {:id-value (value (->widget-id "abc"))}) "The ID is abc."

My initial reaction was that clabango should just allow arbitrary clojure code in its templates. This is a reasonable strategy if the templates are all part of the application. I can't think of why you'd want to have the templates be user defined anyway, and the code generation abilities of Lisp-like languages will make turning templates into compiled code fairly simple. But clabango is what it is, and such a radical departure would yield something other than clabango.

So instead I implemented a simple filter that executes a one argument function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(require 'clabango.filters)
(alias 'filters 'clabango.filters)

(require 'clojure.tools.reader.edn)
(alias 'edn 'clojure.tools.reader.edn)

(defn dequote [quoted-string]
  (if (and (> (count quoted-string) 2)
           (= (first quoted-string) \")
           (= (last quoted-string) \"))
    (.substring quoted-string 1 (- (count quoted-string) 1))
    (throw (IllegalArgumentException. ""))))

(defn find-function [ns-function-str]
  (when (string? ns-function-str)
    (try
      @(resolve (edn/read-string (dequote ns-function-str)))
      (catch Exception e
        nil))))

(filters/deftemplatefilter "apply" [node body arg]
  (when body
    (when-let [function (find-function arg)]
      (function body))))

Which allows me to render an ID thus:

user> (parser/render "The ID is {{ id|apply:\"user/value\" }}."
                     {:id (->widget-id "abc")})
"The ID is abc."

I am deeply disappointed with clabango. My initial hesitation was that moving from clabango to something else would effectively mean tracking changes in luminus on an ongoing basis, which is too high an overhead for a one-person hobby project. But luminus doesn't seem to have any libraries of its own, just some templates that make starting a project straightforward.

Whatever I choose to do, I will write up in a follow-on blog post.