Jud202
Jud202

Reputation: 3

Setting an attribute in a Nokogiri::XML::NodeSet with css

I was trying to set an attribute for a specific element in an xml file and I was having success using

doc.css('Object').attr("Id").value = timestamp

This was fine until the situation where 'Object' doesn't exist causing an exception in the program and quitting. To avoid that I wanted to use Nodeset as it'll just be empty instead.

doc.css('Object').each do |element|
    element.attr("Id").value = timestamp
end

However this returns with the error that value= is an undefined method. It's probably something simple but I'm new to Ruby and CSS so any help would be great.

Upvotes: 0

Views: 1920

Answers (1)

the Tin Man
the Tin Man

Reputation: 160551

The problem has little to do with CSS, since Nokogiri uses CSS selectors as an alternative to using XPath selectors, and both are only used to provide a path to a node, or nodes.

It looks like you're overthinking this, and making it harder than it needs to be. Here's what I'd do:

require 'nokogiri'

doc = Nokogiri::XML(<<EOT)
<xml>
  <foo>
    <Object>bar</Object>
  </foo>
</xml>
EOT

doc.at('Object')["Id"] = Time.now.to_s

Looking at doc at this point shows:

puts doc.to_xml
# >> <?xml version="1.0"?>
# >> <xml>
# >>   <foo>
# >>     <Object Id="2014-01-28 19:13:32 -0700">bar</Object>
# >>   </foo>
# >> </xml>

It's really important to understand the difference between at, at_css and at_xpath, which return the first matching Node, and search, css and xpath, which return NodeSets. A NodeSet is akin to an array containing Nodes. When you know that, your statement:

doc.css('Object').attr("Id").value = timestamp

won't make much sense, especially since that's not how the attr method is defined:

attr(key, value = nil, &blk) 

You'd need to use:

doc.css('Object').attr("Id", value)

which would assign value to all Id attributes for every <Object> node in the document.

But, again, that's not the right choice, instead you should use at or at_css to return the single node.

This was fine until the situation where 'Object' doesn't exist

If no <Object> node exists, then it gets more interesting, and you have to determine what to do. You can insert it, or, you can simply move along and do nothing.

To see if a node exists is simple:

object_node = doc.at('Object')
if object_node
  object_node['Id'] = Time.now.to_s
else
  # ... insert it
end

To insert a node involves locating the place you want to insert it, then add it:

doc.at('foo').add_child("<Object Id='#{ Time.now }'>baz</Object>")
puts doc.to_xml
# >> <?xml version="1.0"?>
# >> <xml>
# >>   <foo>
# >>     <Object>bar</Object>
# >>   <Object Id="2014-01-28 19:39:38 -0700">baz</Object></foo>
# >> </xml>

I didn't try to make the XML output pretty, which isn't important in XML, it merely needs to be syntactically correct.

Also note that it's possible to insert a node, or nodes, by defining them as a string of XML. Nokogiri will parse it into the appropriate XML and graft it in where you said. You could also go the long route by define a NodeSet or Node, then inserting it, but, in general, that makes uglier code and causes you to do a lot more work, which results in less readable code for those who follow in your footsteps maintaining the source.

Upvotes: 2

Related Questions