Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add elements to Nokogiri XML Builder outside of nesting

Tags:

xml

ruby

nokogiri

So I'm trying to build an XML file, and the nature of how it must be built means that I have to add elements outside of the natural nested structure. For example:

builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
  xml.Data {
    xml.Groups do |inner|
      inner.send(:"GroupType", "test")
    end
    # Insert child element into Groups element.
  }
end

I want the XML to look like:

<Data>
  <Groups>
    <GroupType>test</GroupType>
    <AnotherNode>13</AnotherNode>
  </Groups>
</Data>

Where <AnotherNode> is added where my comment is in the first code sample.

Should be relatively simple but I can't figure it out for the life of me. Presumably I need to be able to search for the block, or, have a reference to it when I form it and then use that?

On a side note, I've taken over a project that has a bunch of Nokogiri stuff in it already, it has selectors like:

xml_file = Nokogiri::XML(xml)

(xml_file/:RootNode).each do |root|
  (root/:SomeItem).each do |si|
    ...
  end
end

.. yet I can't find anything like that in the docs? what's it all about?

like image 817
Mike Campbell Avatar asked Nov 20 '25 12:11

Mike Campbell


2 Answers

Bumped into this two years after the fact, and thought the accepted answer was way more convoluted than it needs to be.

The problem is, you're building an XML fragment here, while by default Nokogiri's builder assumes you're building a complete document. XML documents only have one root node.

One option would be to build the complete document as follows, then grab the root's children:

<DISPOSABLE_OUTER_NODE>
  <Data>
    <Groups>
      <GroupType>test</GroupType>
     </Groups>
  </Data>
  <AnotherNode>13</AnotherNode>
</DISPOSABLE_OUTER_NODE>

But there's a better, "fragmentary" way. You build an empty fragment, then have builder work on that rather than a new document:

frag = Nokogiri::XML::DocumentFragment.parse("")

Nokogiri::XML::Builder.with( frag ){ |b|
  b.Data do
    b.Groups do
      b.GroupType "test"
    end
  end

  b.AnotherNode "13"
}

puts frag.to_xml

Then just stick it into whatever document you're modifying.

Maybe with is a latecomer to Nokogiri. But it's an elegant solution to a problem I was having, so I thought it belonged here.

like image 69
Bryce Anderson Avatar answered Nov 23 '25 03:11

Bryce Anderson


Using Nokogiri::XML::Builder only, you need to do it this way:

require 'nokogiri'

builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
  xml.Data {
    xml.Groups {
      xml.GroupType "test"
      xml.AnotherNode "13"
    }
  }
end

puts builder.to_xml
Which outputs:

=> <?xml version="1.0" encoding="UTF-8"?>
   <Data>
     <Groups>
       <GroupType>test</GroupType>
       <AnotherNode>13</AnotherNode>
     </Groups>
   </Data>

Builder is a DSL and designed as a convenience, with a limited set of capabilities. If you don't want to do it the "Builder-way" you can do it "old-school" and take an existing XML Node, and build upon it:

require 'nokogiri'

builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
  xml.Data {
    xml.Groups {
      xml.GroupType "test"
    }
  }
end

This created the base XML needed inside the Builder object. Render it as XML and reparse it into a Nokogiri::XML::Document, then work on it:

doc = Nokogiri::XML(builder.to_xml)
doc.at('GroupType').add_next_sibling("<AnotherNode>13</AnotherNode>")
puts doc.to_xml

=> <?xml version="1.0" encoding="UTF-8"?>
   <Data>
     <Groups>
       <GroupType>test</GroupType><AnotherNode>13</AnotherNode>
     </Groups>
   </Data>

doc = Nokogiri::XML(builder.to_xml)
doc.at('Groups').add_child("<AnotherNode>13</AnotherNode>")
puts doc.to_xml

=> <?xml version="1.0" encoding="UTF-8"?>
   <Data>
     <Groups>
       <GroupType>test</GroupType>
     <AnotherNode>13</AnotherNode></Groups>
   </Data>

Either of the two above ways render the same thing syntactically, they're just cosmetically different.

You can even get convoluted and funky and do it this way:

builder = Nokogiri::XML::Builder.with(
  Nokogiri::XML(
    builder.to_xml
  ).at('Groups') << "<AnotherNode>13</AnotherNode>"
)
puts builder.to_xml

=> <?xml version="1.0" encoding="UTF-8"?>
   <Data>
     <Groups>
       <GroupType>test</GroupType>
     <AnotherNode>13</AnotherNode></Groups>
   </Data>
like image 20
the Tin Man Avatar answered Nov 23 '25 04:11

the Tin Man