I have many levels of nested hash like:
{ :foo => 'bar', :foo1 => { :foo2 => 'bar2', :foo3 => 'bar3', :foo4 => { :foo5 => 'bar5' }}}
How can I convert them into an XML like this?:
<foo>bar</foo>
<foo1>
<foo2>bar2</foo2>
<foo3>bar3</foo3>
<foo4>
<foo5>bar5</foo5>
</foo4>
</foo1>
I have tried the xml.send
method, but it converts the above nested hash to:
<foo1 foo3="bar3" foo4="foo5bar5" foo2="bar2"/>
<foo>bar</foo>
How about this?
class Hash
def to_xml
map do |k, v|
text = Hash === v ? v.to_xml : v
"<%s>%s</%s>" % [k, text, k]
end.join
end
end
h.to_xml
#=> "<foo>bar</foo><foo1><foo2>bar2</foo2><foo3>bar3</foo3><foo4><foo5>bar5</foo5></foo4></foo1>"
The accepted is a clean solution, but the below really does 'use' Nokogiri to construct XML from a Hash with special handling for attributes:
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>
If you are using Rails you could use the built-in to_xml method.
c = { :foo => 'bar', :foo1 => { :foo2 => 'bar2', :foo3 => 'bar3', :foo4 => { :foo5 => 'bar5' }}}
xml = c.to_xml
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <foo>bar</foo>\n <foo1>\n <foo2>bar2</foo2>\n <foo3>bar3</foo3>\n <foo4>\n <foo5>bar5</foo5>\n </foo4>\n </foo1>\n</hash>\n"