ActiveRecord .from_xml upgrade


The gist of this tiny code snip... 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 <id> tag, then finds or creates an author with a matching author.remote_id. Then the code updates the, and return an array of authors.


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) = *node.from_xml(Author, :id, :name)!    end

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

(Note, also, that <tag> records, for example, should be shared between many <post> 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 =        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 "". 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!