Wednesday, May 16, 2012

Emitting XML from Clojure using clojure.data.xml.

I have a need to serialize a Clojure data structure to XML, but I'm stumbling on how to emit a collection. To make this more concrete, assume I have a Clojure map as follows:



    (def parking-lot {:parking-lot #{
{:car {:color "red", :make "nissan", :year 2003}}
{:car {:color "blue", :make "toyota", :year 2001}}
{:car {:color "black", :make "honda", :year 2010}}}})


This is just a map with one element whose value is a set of "car" items. Now, let's assume that I want to serialize this map to produce the following XML:



    <? xml version="1.0" ?>
<parking-lot>
<car color="red" make="nissan" year="2003" />
<car color="blue" make="toyota" year="2001" />
<car color="black" make="black" year="2010" />
</parking-lot>


Searching the web for docs on how to best parse/emit XML in Clojure led me to the clojure.data.xml library, so that's what I'm using.



Here's a trivial example of how to use clojure.data.xml to emit some XML:



    REPL> (use 'clojure.data.xml)

=> nil
REPL> (emit-str (element :parking-lot {}))

=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<parking-lot></parking-lot>"


A slightly more complicated example:



    REPL> (emit-str (element :parking-lot {}
(element :car {:color "yellow" :make "ford" :year "2000"})
(element :car {:color "purple" :make "chevrolet" :year "1977"})))

=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<parking-lot>
<car color=\"yellow\" year=\"2000\" make=\"ford\"></car>
<car color=\"purple\" year=\"1977\" make=\"chevrolet\"></car>
</parking-lot>"


Here you see that the 3rd argument to the 'element' function can indeed be variable number of calls to 'element'. Without looking deeply at the docs for the 'element' function, I guess I assumed that 'element' would be overloaded to also take a sequence of element items as its 3rd argument. And so, without looking at the docs, I just went ahead and whipped up a small block of code that would do just that:



    REPL> (emit-str (element :parking-lot {}
(map (fn [car] (let [carattrs (:car car)]
(element :car {:color (:color carattrs),
:make (:make carattrs),
:year (:year carattrs)})))
(:parking-lot parking-lot))))


I was hoping this would work, but alas it does not. The problem is that the 3rd argument to the 'element' function cannot be a sequence itself.



So, at this point I'm a bit stumped as to what to do. I'm still relatively new to Lisp and Clojure, so go easy on me if my next thought is moronic. So my thought is, would this be a use case for a macro? I.e., should I create a macro that takes as its input a set (or any coll for that matter) of cars, and outputs its items as individual s-expressions? So for example, I'm thinking in my mind of a macro that behaves like this:



    REPL> (defmacro flatten-as-elements [coll-of-cars] ...)
REPL> (flatten-as-elements #{ {:color "blue" :model "honda" year "2000"}
{:color "yellow" :model "ford" year "2011"}})
=> (element :car {:color "blue" :model "honda" year "2000"})
(element :car {:color "yellow" :model "ford" year "2011"})


In my mind at least, the output of such a macro would fit as the 3rd argument to the 'element' function, and would produce my desired goal. Of course, my worry is that there is some completely obvious solution that I'm overlooking, and that using a macro here is misguided. Any help from the SO community is greatly appreciated! Thank you in advance!



-Paul





No comments:

Post a Comment