0

I have a collection of OpenStruct elements using which I need to build an XML with help of Nokogiri.

collection = [
OpenStruct.new(:catalogStoreNumber => '657758',
:catalogStoreId => 'CTH6536',
:catalogStoreLocation => 'UnitedStates', 
:catalogOwnerId => 'TYCT11190',
:catalogOwner => 'McGrawHill Pub.',
:catalogList => OpenStruct.new(
  :catalogProductInfo => OpenStruct.new(
  :productType => 'Book',
  :productName => 'The Client', 
  :productAuthorized => 'Y', 
  :productId => 'BKSUS113246A',
  :productVerificationCode => '4546747', 
  :productPurcTransactionTime => '2012-05-21T13:36:38+05:30',
  :productAuditDetails => OpenStruct.new(
    :productAuditNo => '1',
    :prodHandledByUser => 'StoreUserS14',
    :productAuditTime => '2012-05-21T13:36:38+05:30',
    :productAuditAdminId => 'McGr1132', 
    :productPurchaseRate => '50.14 Prcnt',
    :productSystemLoggerId => 'UNX-NETW4536'
    ), 
  :productAuditDetails => OpenStruct.new(
    :productAuditNo => '2',
    :prodHandledByUser => 'OnlineUserOn008',
    :productAuditTime => '2012-05-23T16:16:08+05:30',
    :productAuditAdminId => 'McGr1132', 
    :productPurchaseRate => '84.86 Prcnt',
    :productSystemLoggerId => 'UNX-NETW4536'
    )
  ),
:catalogProductInfo => OpenStruct.new(
  :productType => 'Pen',
  :productName => 'Reynolds'
  :productAuthorized => 'N', 
  :productId => 'PNSUS228886B',
  :productVerificationCode => '2330076', 
  :productPurcTransactionTime => '2012-04-22T15:06:18+04:30',
  :productAuditDetails => OpenStruct.new(
    :productAuditNo => '1',
    :prodHandledByUser => 'CCUserA14',
    :productAuditTime => '2012-04-26T13:36:38+05:30',
    :productAuditAdminId => 'ReyGr1132', 
    :productPurchaseRate => '20.19 Prcnt',
    :productSystemLoggerId => 'WIN-NETW4536'
    )
  )
 )
)] 

I tried with below code .. as per your answer (handpick of elements)

builder = Nokogiri::XML::Builder.new do |xml|
    xml.CatalogOrder do 
    collection.each do |ctlg|
     xml.CatalogStoreNumber ctlg.catalogStoreNumber
     xml CatalogStoreId ctlg.catalogStoreId
     xml.CatalogOwnerId ctlg.catalogOwnerid
     xml.CatalogOwner ctlg.catalogOwner
      xml.CatalogList do
        prod_count = 0
        aud_list_count = 0 
        collection.each do |prod|
          info = prod.catalogList[0].catalogProductInfo
          xml.ProductInfo do
            xml.ProductType info.productType
            xml.ProductName info.productName
            xml.ProductId   info.productId
            xml.ProductVerificationCode info.ProductVerificationCode
            xml.ProductPurcTransactionTime info.productPurcTransactionTime
            xml.ProductAuditDetails do
            collection.each do |aud_dtl|
             aud_info = aud_dtl.catalogList[0].catalogProductinfo[0].productAuditDetails
             xml.ProductAuditNo aud_info.productAuditNo
             xml.ProdHandledByUser aud_info.prodHandledByUser
             xml.ProductAuditTime aud_info.productAuditTime
             xml.ProductAuditAdminId aud_info.productAuditAdminId
             xml.ProductPurchaseRate aud_info.productPurchaseRate
             xml.ProductSystemLoggerId aud_info.productSystemLoggerId
            # Do whatever you must above to concoct your ProductId
          end
         aud_list_count = aud_list_count + 1
        end
        prod_count = prod_count + 1
      end
    end
  end

  puts builder.to_xml

I need the Output as below...

<CatalogOrder>
 <CatalogStoreNumber>657758</CatalogStoreNumber>
 <CatalogStoreId>CTH6536</CatalogStoreId>
 <CatalogStoreLocation>UnitedStates</CatalogStoreLocation>
 <CatalogOwnerId>TYCT11190</CatalogOwnerId>
 <CatalogOwner>McGrawHill Pub.</CatalogOwner>
 <CatalogList>
   <CatalogProductInfo>
     <ProductType>Book</ProductType>
     <ProductName>The Client</ProductName>
     <ProductAuthorized>Y</ProductAuthorized>
     <ProductId>BKSUS113246A</ProductId>
     <ProductVerificationCode>4546747</ProductVerificationCode>
     <ProductPurcTransactionTime>2012-05-21T13:36:38+05:30</ProductPurcTransactionTime>
     <ProductAuditDetails>
       <ProductAuditNo>1</ProductAuditNo>
       <ProdHandledByUser>StoreUserS14</ProdHandledByUser>
       <ProductAuditTime>2012-05-21T13:36:38+05:30</ProductAuditTime>
       <ProductAuditAdminId>McGr1132</ProductAuditAdminId>
       <ProductPurchaseRate>50.14 Prcnt</ProductPurchaseRate>
       <ProductSystemLoggerId>WIN-NETW4536</ProductSystemLoggerId>
    </ProductAuditDetails>
    <ProductAuditDetails>
       <ProductAuditNo>2</ProductAuditNo>
       <ProdHandledByUser>OnlineUserOn008</ProdHandledByUser>
       <ProductAuditTime>2012-05-23T16:16:08+05:30</ProductAuditTime>
       <ProductAuditAdminId>McGr1132</ProductAuditAdminId>
       <ProductPurchaseRate>84.86 Prcnt</ProductPurchaseRate>
       <ProductSystemLoggerId>UNX-NETW4536</ProductSystemLoggerId>
    </ProductAuditDetails>
   </CatalogProductInfo>
   <CatalogProductInfo>
     <ProductType>Pen</ProductType>
     <ProductName>Reynolds</ProductName> 
     <ProductAuthorized>N</ProductAuthorized>        
     <ProductId>PNSUS228886B</ProductId>
     <ProductVerificationCode>2330076</ProductVerificationCode>
     <ProductPurcTransactionTime>2012-04-22T15:06:18+04:30</ProductPurcTransactionTime>
     <ProductAuditDetails>
       <ProductAuditNo>1</ProductAuditNo>
       <ProdHandledByUser>CCUserA14</ProdHandledByUser>
       <ProductAuditTime>2012-04-26T13:36:38+05:30</ProductAuditTime>
       <ProductAuditAdminId>ReyGr1132</ProductAuditAdminId>
       <ProductPurchaseRate>20.19 Prcnt</ProductPurchaseRate>
       <ProductSystemLoggerId>WIN-NETW4536</ProductSystemLoggerId>
     </ProductAuditDetails>
   </CatalogProductInfo>
 </CatalogList>
</CatalogOrder> 

I tried to loop in the nested Array of OpenStruct of elements, but couldn't land on right logic for that ..

Ref.. How to add child nodes in NodeSet using Nokogiri

Community
  • 1
  • 1
user1023627
  • 183
  • 1
  • 3
  • 12
  • Why are you using `OpenStruct` instead of just a `Hash`, out of curiosity? – Phrogz Jun 25 '12 at 16:27
  • **Error**: You ask for the output to include `CRSUS113246A`, but this does not appear anywhere in your input. How is the `ProductId` calculated? – Phrogz Jun 25 '12 at 16:53
  • -1 for a poorly-specified question. You have not given a full representation of either the sample input nor desired output, and as such it is hard to know exactly what you want. – Phrogz Jun 25 '12 at 16:59
  • I wanted the data to be organized in way that is easy to construct the XML for multiple scenario's. I don't see the use of YAML in this case as it occupies my workspace (i.e,. multiple ".yml" for multiple scenario's). So, i am searching for alternative for YML. I don't see the scalability of OpenStruct for larger sets of data, but it is sufficient for me right now. Could this be achieved using "Json" to organize multiple sets of data ? – user1023627 Jun 25 '12 at 17:23
  • I would just use Hash literals. – Phrogz Jun 26 '12 at 13:24

2 Answers2

3

Setup Code

require 'ostruct'
require 'nokogiri'

collection = [
  OpenStruct.new(
    :catalogStoreNumber => '657758',
    :catalogStoreId => 'CTH6536',
    :catalogStoreLocation => 'UnitedStates', 
    :catalogOwnerId => 'TYCT11190',
    :catalogOwner => 'McGrawHill Pub.',
    :catalogList => OpenStruct.new(
      :catalogProductInfo => OpenStruct.new(
        :productType => 'Book',
        :productName => 'The Client'
      )
    )
  )
]

If You Want To Hand-Pick Your Elements and Data

builder = Nokogiri::XML::Builder.new do |xml|
  xml.CatalogOrder do
    xml.CatalogList do
      collection.each do |prod|
        info = prod.catalogList.catalogProductInfo
        xml.ProductInfo do
          xml.ProductType info.productType
          xml.ProductName info.productName
          xml.ProductId   "#{prod.catalogOwnerId}-#{prod.catalogStoreNumber}"
          # Do whatever you must above to concoct your ProductId
        end
      end
    end
  end
end

puts builder.to_xml

Output

<?xml version="1.0"?>
<CatalogOrder>
  <CatalogList>
    <ProductInfo>
      <ProductType>Book</ProductType>
      <ProductName>The Client</ProductName>
      <ProductId>TYCT11190-657758</ProductId>
    </ProductInfo>
  </CatalogList>
</CatalogOrder>

If you want a more generic conversion (an XML representation of your OpenStruct hierarchy) see either of the two solutions below:

One Way to Perform Generic Conversion

# Add all entries of an OpenStruct to an XML builder
# Recursively creates sub-nodes for OpenStruct instances
def ostruct_each(ostruct,xml)
  ostruct.instance_variable_get(:@table).each do |field,value|
    if value.is_a?(OpenStruct)
      xml.send(field) do
        ostruct_each(value,xml)
      end
    else
      xml.send(field,value)
    end
  end
end

builder = Nokogiri::XML::Builder.new do |xml|
  xml.CatalogOrder do
    xml.CatalogList do
      collection.each do |prod_info|
        xml.ProductInfo do
          ostruct_each(prod_info,xml)
        end
      end
    end
  end
end

puts builder.to_xml

Output

<?xml version="1.0"?>
<CatalogOrder>
  <CatalogList>
    <ProductInfo>
      <catalogStoreNumber>657758</catalogStoreNumber>
      <catalogStoreId>CTH6536</catalogStoreId>
      <catalogStoreLocation>UnitedStates</catalogStoreLocation>
      <catalogOwnerId>TYCT11190</catalogOwnerId>
      <catalogOwner>McGrawHill Pub.</catalogOwner>
      <catalogList>
        <catalogProductInfo>
          <productType>Book</productType>
          <productName>The Client</productName>
        </catalogProductInfo>
      </catalogList>
    </ProductInfo>
  </CatalogList>
</CatalogOrder>

A Different Way to Generically Convert It

# Create a NodeSet of elements for all attributes in an OpenStruct
# Recursively creates child elements for any value that is an OpenStruct
def ostruct_to_elements(xml_doc,ostruct)
  Nokogiri::XML::NodeSet.new(
    xml_doc,
    ostruct.instance_variable_get(:@table).map do |name,val|
      xml_doc.create_element(name.to_s).tap do |el|
        el << (val.is_a?(OpenStruct) ? ostruct_to_elements(xml_doc,val) : val)
      end
    end
  )
end

builder = Nokogiri::XML::Builder.new do |xml|
  xml.CatalogOrder do
    xml.CatalogList do
      collection.each do |prod_info|
        xml.ProductInfo do
          xml.parent << ostruct_to_elements(xml.doc,prod_info)
        end
      end
    end
  end
end

puts builder.to_xml
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Updated the question .. I have two products inside "CatalogList" and if i add second product. How do i need to code for that ..`collection[0].catalogList.catalogProductInfo[0].productType"` .. doesn't work good.. – user1023627 Jul 07 '12 at 23:01
  • I got this error .. `undefined method `[]' for # (NoMethodError)` Could you please tell me, how to change the above code to handle multiple products ..Thanks – user1023627 Jul 07 '12 at 23:08
  • I am stuck here please help me out – user1023627 Jul 08 '12 at 13:30
  • Edit your question with actual test input, your actual current code, and the actual XML output you want for that test input. – Phrogz Jul 08 '12 at 14:02
  • I updated my question, the code which i tried and also Output i wanted as per your suggestion, I couldn't loop in the Nested Array of OpenStruct Elements, I got stuck here and couldn't proceed further. Please help me out. – user1023627 Jul 09 '12 at 10:46
  • Could you please help out. I'm stuck here. – user1023627 Jul 09 '12 at 13:19
  • I couldn't loop through nested array of OpenStruct elements – user1023627 Jul 09 '12 at 15:44
  • Hi @Phrogz ! I tried modfying the code, but couldn't succeed. – user1023627 Jul 09 '12 at 17:39
  • !Does OpenStruct support Nested Arrays .. Could you please help me out – user1023627 Jul 09 '12 at 20:12
  • Hi Phrogz, Please suggest how to loop in the Nested OpenSturct Arrays – user1023627 Jul 09 '12 at 20:59
  • @user1023627 I have received each and every message you have sent, when you send them. I will work on this problem if and when I have time. You do not need to keep posting comments, as it is doing nothing to further your cause. – Phrogz Jul 09 '12 at 21:22
  • Did you get a chance to look at it – user1023627 Jul 17 '12 at 12:39
  • @user1023627 I just looked at it, and found: a) your source data is not syntactically correct (you are missing a comma); b) your Ruby code is not syntactically correct (you are missing multiple `end` statements, which were hard to find because your indentation was not consistent; c) you are a lot of irrelevant data in your source (you could ask basically the same question with much less code); d) you have your `collection` set to an array of a single item, and I can't imagine why you want that. – Phrogz Jul 17 '12 at 19:41
  • e) You have a missing period on this line: `xml CatalogStoreId ctlg.catalogStoreId` – Phrogz Jul 17 '12 at 19:47
0

Answering your changed question and data.

Here's your source data, summarized:

collection = [
  OpenStruct.new(
    :foo => 'bar',
    :list => OpenStruct.new(
      :catalogProductInfo => OpenStruct.new(...)
      :catalogProductInfo => OpenStruct.new(...)
    )
  )
]

The first thing we notice is that collection is an array, but it only has one item. That doesn't seem very useful.

The second—far more important—thing to notice is that you are attempting to use an OpenStruct (the inner one) as an array. This fails quite fully, as the second value completely overwrites the first:

require 'ostruct'
p OpenStruct.new( a:1, a:2 )
#=> #<OpenStruct a=2>

You can't have two values for the same key in a struct. Instead, you probably wanted an array value inside your object. For example:

root = OpenStruct.new(
  :foo => 'bar',
  :list => [  # this is an array of two distinct objects
    OpenStruct.new( :catalogProductInfo => OpenStruct.new(...) ),
    OpenStruct.new( :catalogProductInfo => OpenStruct.new(...) )
  ]
)

Thirdly, as I noted earlier, I see no good reason for you to be using an OpenStruct here. Instead, just use Hash literals. Combined with the code I provided in my other answer, this is what your data (simplified) and a working solution looks like:

Source Data

MyOrder = {
  catalogStoreNumber: '657758',  # Ruby 1.9 Hash syntax;
  catalogStoreId: 'CTH6536',     # same as :catalogStoreId => 'CTH536'
  catalogList: {
    catalogProductInfo: [
      {
        productType: 'Book',
        productName: 'The Client', 
        productAuditDetails: [
          { productAuditNo: '1', prodHandledByUser: 'StoreUserS14'    },
          { productAuditNo: '2', prodHandledByUser: 'OnlineUserOn008' }
        ]
      },
      {
        productType: 'Pen',
        productName: 'Reynolds',
        productAuditDetails: [
          { productAuditNo: '1', prodHandledByUser: 'CCUserA14' }
        ]
      }
    ]
  }
}

Generic Conversion

# Adds key/value pairs from a Hash to a Nokogiri::XML::Builder
def hash2xml(hash,xml)
  hash.each do |field,value|
    name = field.to_s.sub(/^./,&:upcase) # convert "fooBar" to "FooBar"
    case value
      when Hash  then xml.send(name){ hash2xml(value,xml) }
      when Array then value.each{ |o| xml.send(name){ hash2xml(o,xml) } }
      else            xml.send(name,value)
    end
  end
end

Working Code

builder = Nokogiri::XML::Builder.new do |xml|
  xml.CatalogOrder do
    hash2xml(MyOrder,xml)
  end
end
puts builder.to_xml

Output

<?xml version="1.0"?>
<CatalogOrder>
  <CatalogStoreNumber>657758</CatalogStoreNumber>
  <CatalogStoreId>CTH6536</CatalogStoreId>
  <CatalogList>
    <CatalogProductInfo>
      <ProductType>Book</ProductType>
      <ProductName>The Client</ProductName>
      <ProductAuditDetails>
        <ProductAuditNo>1</ProductAuditNo>
        <ProdHandledByUser>StoreUserS14</ProdHandledByUser>
      </ProductAuditDetails>
      <ProductAuditDetails>
        <ProductAuditNo>2</ProductAuditNo>
        <ProdHandledByUser>OnlineUserOn008</ProdHandledByUser>
      </ProductAuditDetails>
    </CatalogProductInfo>
    <CatalogProductInfo>
      <ProductType>Pen</ProductType>
      <ProductName>Reynolds</ProductName>
      <ProductAuditDetails>
        <ProductAuditNo>1</ProductAuditNo>
        <ProdHandledByUser>CCUserA14</ProdHandledByUser>
      </ProductAuditDetails>
    </CatalogProductInfo>
  </CatalogList>
</CatalogOrder>
Phrogz
  • 296,393
  • 112
  • 651
  • 745