5
\$\begingroup\$
data =
[ { 'name' => 'category1',
 'subCategory' => [ {'name' => 'subCategory1',
 'product' => [ {'name' => 'prodcutName1',
 'desc' => 'desc1'},
 {'name' => 'prodcutName2',
 'desc' => 'desc2'}]
 } ]
 },
 { 
 #category2 ...and so on 
 }
]

Just recently finished a small project with Ruby. I used the above array of hashes to produce a XML.

I think my solution is quite messy nesting loops and builder tags. Can anyone help me out with a more elegant approach?

builder = Nokogiri::XML::Builder.new { |xml|
data.each { |category| # go through every category in the data array
 xml.category {
 xml.name category['name']
 category['subCategory'].each { |subCategory| # go through each subCategory in the category
 xml.subCategory {
 xml.name subCategory['name']
 subCategory['product'].each { |product| # go though each products and print their data
 xml.name product['name']
 xml.desc product['desc']
 }
 }
 }
 }
}
puts builder.to_xml
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked May 16, 2014 at 2:35
\$\endgroup\$
2
  • \$\begingroup\$ Are you able to change the source data hash to a different format? \$\endgroup\$ Commented May 16, 2014 at 4:34
  • \$\begingroup\$ @Phrogz No, the source needs to stay as it is. \$\endgroup\$ Commented May 16, 2014 at 4:44

3 Answers 3

9
\$\begingroup\$

Here's a nice recursive solution, that creates <key>value</key> from 'key'=>'value' entries in your hash. If the value is an array, it instead recurses, using the key name as a wrapper element.

require 'nokogiri'
def process_array(label,array,xml)
 array.each do |hash|
 xml.send(label) do # Create an element named for the label
 hash.each do |key,value|
 if value.is_a?(Array)
 process_array(key,value,xml) # Recurse
 else
 xml.send(key,value) # Create <key>value</key> (using variables)
 end
 end
 end
 end
end
builder = Nokogiri::XML::Builder.new do |xml|
 xml.root do # Wrap everything in one element.
 process_array('category',data,xml) # Start the recursion with a custom name.
 end
end
puts builder.to_xml

When used with this data...

data = [
 { 'name' => 'category1',
 'subCategory' => [
 { 'name' => 'subCategory1',
 'product' => [
 { 'name' => 'productName1',
 'desc' => 'desc1' },
 { 'name' => 'productName2',
 'desc' => 'desc2' } ]
 } ]
 },
 { 'name' => 'category2',
 'subCategory' => [
 { 'name' => 'subCategory2.1',
 'product' => [
 { 'name' => 'productName2.1.1',
 'desc' => 'desc1' },
 { 'name' => 'productName2.1.2',
 'desc' => 'desc2' } ]
 } ]
 },
]

...you get this result:

<?xml version="1.0"?>
<root>
 <category>
 <name>category1</name>
 <subCategory>
 <name>subCategory1</name>
 <product>
 <name>productName1</name>
 <desc>desc1</desc>
 </product>
 <product>
 <name>productName2</name>
 <desc>desc2</desc>
 </product>
 </subCategory>
 </category>
 <category>
 <name>category2</name>
 <subCategory>
 <name>subCategory2.1</name>
 <product>
 <name>productName2.1.1</name>
 <desc>desc1</desc>
 </product>
 <product>
 <name>productName2.1.2</name>
 <desc>desc2</desc>
 </product>
 </subCategory>
 </category>
</root>

However, if I had control over the XML schema, I'd do this instead:

require 'nokogiri'
def process_array(label,array,xml)
 array.each do |hash|
 kids,attrs = hash.partition{ |k,v| v.is_a?(Array) }
 xml.send(label,Hash[attrs]) do
 kids.each{ |k,v| process_array(k,v,xml) }
 end
 end
end
builder = Nokogiri::XML::Builder.new do |xml|
 xml.root{ process_array('category',data,xml) }
end
puts builder.to_xml
<?xml version="1.0"?>
<root>
 <category name="category1">
 <subCategory name="subCategory1">
 <product name="productName1" desc="desc1"/>
 <product name="productName2" desc="desc2"/>
 </subCategory>
 </category>
 <category name="category2">
 <subCategory name="subCategory2.1">
 <product name="productName2.1.1" desc="desc1"/>
 <product name="productName2.1.2" desc="desc2"/>
 </subCategory>
 </category>
</root>

...but perhaps you're dealing with some terrible XML schema like PList.

answered May 16, 2014 at 5:22
\$\endgroup\$
0
\$\begingroup\$

A very clean approach is to use the xml-simple gem. Simply use xml_out with two options:

  • RootName to specify the XML root element
  • AnonymousTag to provide a tag name for your top-level hash

Other options are documented here.

Code:

require 'xmlsimple'
XmlSimple.xml_out(data, {"RootName" => "categories", "AnonymousTag => "category"})

Output:

<categories>
 <category name="category1">
 <subCategory name="subCategory1">
 <product name="prodcutName1" desc="desc1" />
 <product name="prodcutName2" desc="desc2" />
 </subCategory>
 </category>
 <category></category>
</categories>
answered May 27, 2014 at 12:12
\$\endgroup\$
0
\$\begingroup\$

Here is a single function to construct XML from a Hash with special handling for attributes and allows a mix of Array and Hash to represent the elements.

You can also begin the Builder outside and call this function after setting up all necessary namespaces.

require 'nokogiri'
def generate_xml(data, parent = false, opt = {})
 return if data.to_s.empty?
 return unless data.is_a?(Hash)
 unless parent
 # assume that if the hash has a single key that it should be the root
 root, data = (data.length == 1) ? data.shift : ["root", data]
 builder = Nokogiri::XML::Builder.new(opt) do |xml|
 xml.send(root) {
 generate_xml(data, xml)
 }
 end
 return builder.to_xml
 end
 data.each { |label, value|
 if value.is_a?(Hash)
 attrs = value.fetch('@attributes', {})
 # also passing 'text' as a key makes nokogiri do the same thing
 text = value.fetch('@text', '') 
 parent.send(label, attrs, text) { 
 value.delete('@attributes')
 value.delete('@text')
 generate_xml(value, parent)
 }
 elsif value.is_a?(Array)
 value.each { |el|
 # lets trick the above into firing so we do not need to rewrite the checks
 el = {label => el}
 generate_xml(el, parent) 
 }
 else
 parent.send(label, value)
 end
 }
end
puts generate_xml(
 {'myroot' => 
 {
 'num' => 99, 
 'title' => 'something witty', 
 'nested' => { 'total' => [99, 98], '@attributes' => {'foo' => 'bar', 'hello' => 'world'}}, 
 'anothernest' => {
 '@attributes' => {'foo' => 'bar', 'hello' => 'world'}, 
 'date' => [
 'today', 
 {'day' => 23, 'month' => 'Dec', 'year' => {'y' => 1999, 'c' => 21}, '@attributes' => {'foo' => 'blhjkldsaf'}}
 ]
 }
 }})
puts puts
puts generate_xml({
 'num' => 99, 
 'title' => 'something witty', 
 'nested' => { 'total' => [99, 98], '@attributes' => {'foo' => 'bar', 'hello' => 'world'}}, 
 'anothernest' => {
 '@attributes' => {'foo' => 'bar', 'hello' => 'world'}, 
 'date' => [
 'today', 
 {'day' => [23,24], 'month' => 'Dec', 'year' => {'y' => 1999, 'c' => 21}, '@attributes' => {'foo' => 'blhjkldsaf'}}
 ]
 }
 })

And the resulting XML output:

<?xml version="1.0"?>
<myroot>
 <num>99</num>
 <title>something witty</title>
 <nested foo="bar" hello="world">
 <total>99</total>
 <total>98</total>
 </nested>
 <anothernest foo="bar" hello="world">
 <date>today</date>
 <date foo="blhjkldsaf">
 <day>23</day>
 <month>Dec</month>
 <year>
 <y>1999</y>
 <c>21</c>
 </year>
 </date>
 </anothernest>
</myroot>
<?xml version="1.0"?>
<root>
 <num>99</num>
 <title>something witty</title>
 <nested foo="bar" hello="world">
 <total>99</total>
 <total>98</total>
 </nested>
 <anothernest foo="bar" hello="world">
 <date>today</date>
 <date foo="blhjkldsaf">
 <day>23</day>
 <day>24</day>
 <month>Dec</month>
 <year>
 <y>1999</y>
 <c>21</c>
 </year>
 </date>
 </anothernest>
</root
answered Dec 8, 2014 at 20:22
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.