Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.
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
share|improve this question

migrated from stackoverflow.com May 23 at 18:15

This question came from our site for professional and enthusiast programmers.

    
Are you able to change the source data hash to a different format? –  Phrogz May 16 at 4:34
    
@Phrogz No, the source needs to stay as it is. –  yummisashimi May 16 at 4:44

2 Answers 2

up vote 3 down vote accepted

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.

share|improve this answer

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>
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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