ActiveRecord .from_xml upgrade


#1

Rubies:

The gist of this tiny code snip…

http://gist.github.com/75525

…is a light but flexible DSL that converts XML - typically output by
to_xml()

  • into an ActiveRecord object model.

==create or update records==

Here’s the simplest example:

xml ='<photos>
        <photo>
          <id>323285</id>
        </photo>
        <photo>
          <id>323310</id>
        </photo>
...
      </photos>'

doc = Nokogiri::XML(xml)
photos = doc.from_xml(Photo, :id)

(Note that from_xml{} is a member of a Node, not of your Model.)

That code created new Photo records with matching IDs. If any record
were
already there, the code would update it instead.

==rename fields and pass in data==

Here’s the next more complicated example:

authors = node.from_xml(Author, [:id, :remote_id], :name)

The code reads an tag, then finds or creates an author with a
matching author.remote_id. Then the code updates the author.name, and
return an array of authors.

==associations==

from_xml takes an optional &block, and yields into this the record under
construction, before its .save! call. Use this block to seek nested
data, and
plug them into their parent record:

doc.from_xml Post, :id, :title, :body do |post, node, *|
  post.tags = node.from_xml(Tag, :id, :name)
  post.author = *node.from_xml(Author, :id, :name)
  post.save!
end

from_xml{} will call that block each time it finds a (top-level)
record,
and each nested node.from_xml{} call will only find records inside that
main record.

(Note, also, that records, for example, should be shared between
many
records, and your XML will probably just duplicate them many
times, but
from_xml(Tag) knows to fold them all back together again…)

The splat operator * threw away three more arguments - they were the
string
values of the id, title, and body fields.

==raw XML==

To scan your XML with very similar abilities, but without using a Model
with the
correct name to match your XPath, write the XPath directly into the
lower-level
convert{} method:

  node.convert 'tags/tag', :id, :name do |n, id, name|
    tag = Tag.find_or_initialize_by_id(id)
    tag.update_attribute :name, name
     # or tag.attributes = n.data
    post.tags << tag
  end

That block shows form_tag{} “unrolled” into its low-level behavior.
convert{}
takes an XPath query, relative to the current node, and a list of fields
(and
their renamers) to extract. Then it yields the detected node (don’t call
it
“node”!) into its |goal posts|, with the string value of each detected
field.

Your block could have done something more complex, but this one merely
simulated
form_tag{} by reconstituting and updating a Tag record, then inserted it
into
some outer post object.

One more detail - the renamed fields, and their string values, are also
available as a hash. To avoid even more extra arguments into our goal
posts, the
committee stashed them into the passed node, as an attribute called
“node.data”.
So the little comment shows how to update all your Model attributes at
once.

==what about to_xml?==

One ActiveRecord FAQ goes, “Why can’t from_xml take the same arguments
as
to_xml?” The reason is creation is harder than just reading an existing
object
model. While a future version of from_xml{} could indeed learn to follow
model
associations, and could take a big blob of nested hashes, like most
other
ActiveRecord methods, the committee does not foresee this DSL exactly
matching
the input to to_xml(). That is a goal for further research on both
sides!