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
-
\$\begingroup\$ Are you able to change the source data hash to a different format? \$\endgroup\$Phrogz– Phrogz2014年05月16日 04:34:56 +00:00Commented May 16, 2014 at 4:34
-
\$\begingroup\$ @Phrogz No, the source needs to stay as it is. \$\endgroup\$yummisashimi– yummisashimi2014年05月16日 04:44:46 +00:00Commented May 16, 2014 at 4:44
3 Answers 3
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.
A very clean approach is to use the xml-simple
gem. Simply use xml_out
with two options:
RootName
to specify the XML root elementAnonymousTag
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>
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