I'm new to Clojure and would like to convert an XML I have into an edn object.
The XML file I reads:
<Vehicle>
<Model>Toyota</Model>
<Color>Red</Color>
<Loans>
<Reoccuring>Monthly</Reoccuring>
<Owners>
<Owner>Bob</Owner>
</Owners>
</Loans>
<Tires>
<Model>123123</Model>
<Size>23</Size>
</Tires>
<Engine>
<Model>30065</Model>
</Engine>
</Vehicle>
And I have saved it as 'test/resources/vehicle.xml
Ultimately, I would like to have a EDN object that looks like:
:Vehicle
:Model "Toyota"
:Color "Red"
:Loans
:Reoccuring "Monthly"
:Owners
:Owner "Bob"
:Tires
:Model 123123
:Size 23
:Engine
:Model 30065
So far, what I have tried in Clojure is the parse method:
(def xml-parser
(parse "<Vehicle><Model>Toyota</Model><Color>Red</Color><Loans><Reoccuring>Monthly</Reoccuring><Owners><Owner>Bob</Owner></Owners></Loans><Tires><Model>123123</Model><Size>23</Size></Tires><Engine><Model>30065</Model></Engine></Vehicle>"))
However, this returns a Clojure hash that looks like:
{:tag :Vehicle, :attrs nil, :content [{:tag :Model, :attrs nil, :content ["Toyota"]} {:tag :Color, :attrs nil, :content ["Red"]} {:tag :Loans, :attrs nil, :content [{:tag :Reoccuring, :attrs nil, :content ["Monthly"]} {:tag :Owners, :attrs nil, :content [{:tag :Owner, :attrs nil, :content ["Bob"]}]}]} {:tag :Tires, :attrs nil, :content [{:tag :Model, :attrs nil, :content ["123123"]} {:tag :Size, :attrs nil, :content ["23"]}]} {:tag :Engine, :attrs nil, :content [{:tag :Model, :attrs nil, :content ["30065"]}]}]}
I'm having trouble with this initial step of conversion. Thank you for your help in advance.
The data you have is in Enlive format. Use clojure.pprint/pprint
to see a nicer format:
{:tag :Vehicle,
:attrs nil,
:content
[{:tag :Model, :attrs nil, :content ["Toyota"]}
{:tag :Color, :attrs nil, :content ["Red"]}
{:tag :Loans,
:attrs nil,
:content
[{:tag :Reoccuring, :attrs nil, :content ["Monthly"]}
{:tag :Owners,
:attrs nil,
:content [{:tag :Owner, :attrs nil, :content ["Bob"]}]}]}
{:tag :Tires,
:attrs nil,
:content
[{:tag :Model, :attrs nil, :content ["123123"]}
{:tag :Size, :attrs nil, :content ["23"]}]}
{:tag :Engine,
:attrs nil,
:content [{:tag :Model, :attrs nil, :content ["30065"]}]}]}
The problem is that your desired output is not actually legal EDN data format. However, you can use the tupelo.forest
library to convert among several data formats:
First declare the data and parse it into Enlive format:
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require
[tupelo.parse.xml :as xml]
[tupelo.forest :as tf])
)
(def xml-str
"<Vehicle>
<Model>Toyota</Model>
<Color>Red</Color>
<Loans>
<Reoccuring>Monthly</Reoccuring>
<Owners>
<Owner>Bob</Owner>
</Owners>
</Loans>
<Tires>
<Model>123123</Model>
<Size>23</Size>
</Tires>
<Engine>
<Model>30065</Model>
</Engine>
</Vehicle> ")
verify the result
(dotest
(let [data-enlive (xml/parse xml-str)]
(is= data-enlive
{:tag :Vehicle,
:attrs {},
:content [{:tag :Model, :attrs {}, :content ["Toyota"]}
{:tag :Color, :attrs {}, :content ["Red"]}
{:tag :Loans,
:attrs {},
:content [{:tag :Reoccuring, :attrs {}, :content ["Monthly"]}
{:tag :Owners,
:attrs {},
:content [{:tag :Owner, :attrs {}, :content ["Bob"]}]}]}
{:tag :Tires,
:attrs {},
:content [{:tag :Model, :attrs {}, :content ["123123"]}
{:tag :Size, :attrs {}, :content ["23"]}]}
{:tag :Engine,
:attrs {},
:content [{:tag :Model, :attrs {}, :content ["30065"]}]}]})
convert to Hiccup format:
(is= (tf/enlive->hiccup data-enlive)
[:Vehicle
[:Model "Toyota"]
[:Color "Red"]
[:Loans [:Reoccuring "Monthly"]
[:Owners [:Owner "Bob"]]]
[:Tires [:Model "123123"]
[:Size "23"]]
[:Engine [:Model "30065"]]])
You may also like the "bush" format:
(is= (tf/enlive->bush data-enlive)
[{:tag :Vehicle}
[{:tag :Model, :value "Toyota"}]
[{:tag :Color, :value "Red"}]
[{:tag :Loans}
[{:tag :Reoccuring, :value "Monthly"}]
[{:tag :Owners} [{:tag :Owner, :value "Bob"}]]]
[{:tag :Tires}
[{:tag :Model, :value "123123"}]
[{:tag :Size, :value "23"}]]
[{:tag :Engine} [{:tag :Model, :value "30065"}]]])
or the more detailed "tree" format
(is= (tf/enlive->tree data-enlive)
{:tag :Vehicle,
:tupelo.forest/kids
[{:tag :Model, :value "Toyota", :tupelo.forest/kids []}
{:tag :Color, :value "Red", :tupelo.forest/kids []}
{:tag :Loans,
:tupelo.forest/kids
[{:tag :Reoccuring, :value "Monthly", :tupelo.forest/kids []}
{:tag :Owners,
:tupelo.forest/kids
[{:tag :Owner, :value "Bob", :tupelo.forest/kids []}]}]}
{:tag :Tires,
:tupelo.forest/kids
[{:tag :Model, :value "123123", :tupelo.forest/kids []}
{:tag :Size, :value "23", :tupelo.forest/kids []}]}
{:tag :Engine,
:tupelo.forest/kids
[{:tag :Model, :value "30065", :tupelo.forest/kids []}]}]})
))
See the Tupelo Forest docs for full information.
The above code was run using this template project.
If you are looking for a hierarchical map style output, you can kludge together something like so:
(ns tst.demo.core
(:use tupelo.core tupelo.test)
(:require [clojure.walk :as walk]))
(dotest
(let [data [:Vehicle
[:Model "Toyota"]
[:Color "Red"]
[:Loans
[:Reoccuring "Monthly"]
[:Owners
[:Owner "Bob"]]]
[:Tires
[:Model "123123"]
[:Size "23"]]
[:Engine
[:Model "30065"]]]
mappy (walk/postwalk
(fn [item]
(if (vector? item)
(if (= 2 (count item))
(conj {} item)
{(first item)
(into {} (rest item))})
item))
data)]
with test
(is= mappy
{:Vehicle
{:Model "Toyota",
:Color "Red",
:Loans {:Reoccuring "Monthly"
:Owners {:Owner "Bob"}},
:Tires {:Model "123123"
:Size "23"},
:Engine {:Model "30065"}}})))
Although this is pretty fragile as written.