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
\$\endgroup\$
2
  • \$\begingroup\$ Are you able to change the source data hash to a different format? \$\endgroup\$
    – Phrogz
    Commented May 16, 2014 at 4:34
  • \$\begingroup\$ @Phrogz No, the source needs to stay as it is. \$\endgroup\$
    – yummisashimi
    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.

\$\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>
\$\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
\$\endgroup\$
0

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.