Ruby on Rails - ActiveRecord#build_from_xml function
I was playing with the new to_xml
feature of Ruby on Rails and I found myself wondering... if you can create XML from ActiveRecord objects, why can't you create ActiveRecord objects from XML?
After searching for a while in the RoR Documentation I wasn't able to find the inverse functionality of to_xml
. So now, it seems, I have an opportunity to contribute back to the Rails community with an a functional improvement of my own. I announce to you the build_from_xml
method to ActiveRecord.
Just place the below code in your config/environment.rb
file.
require "rexml/document"
module ActiveRecord
class Base
def self.build_from_xml(xml)
xml = REXML::Document.new(xml) if xml.class == String
ar = self.new
xml.elements[1].elements.each do | ele |
sym = ele.name.underscore.to_sym
# An association
if ele.has_elements?
klass = self.reflect_on_association(sym).klass
ar.__send__(sym) << klass.build_from_xml(ele)
# An attribute
else
ar[sym] = ele.text
end
end
return ar
end
end
end
You can call this from the main class of any ActiveRecord object. Here is an example.
This ruby code:
firm_xml = File.new("firm_data.xml").read
firm = Firm.build_from_xml(firm_xml)
Will convert this XML file into a fully functional ActiveRecord object, including the associations.
<firm>
<rating type="integer">1</rating>
<name>37signals</name>
<clients>
<client>
<rating type="integer">1</rating>
<name>Summit</name>
<id type="integer">1</id>
<firm-id type="integer">1</firm-id>
</client>
<client>
<rating type="integer">1</rating>
<name>Microsoft</name>
<id type="integer">2</id>
<firm-id type="integer">1</firm-id>
</client>
</clients>
<accounts>
<account>
<id type="integer">1</id>
<firm-id type="integer">1</firm-id>
<credit-limit type="integer">50</credit-limit>
</account>
</accounts>
<id type="integer">1</id>
</firm>
You may have noticed one caveat. This function accepts well formed XML code only that conforms to your model. If it doesn't, it may produce unpredictable results but will probably raise the usual ActiveRecord exceptions in most non-trivial error cases. Oh, and it requires REXML, but you knew that already right.
I will probably convert this to a plugin in the not-to-distant future. That is if the code isn't included in Rails' release branch (hint, hint).
Reader Comments (10)
Thank you that is very useful I was just getting to writing something similar when I discovered this. A great start!
I have enhanced the code to deal with:-
* Records that have :null => false, so that their default is set if XML has no value, and :null=> false for that particular column.
* Ignoring relations defined in the XML that aren't part of the models
* Forcing XML names to match with object names
* XML data containing multiple record data (returns a list of instances)
* Also I had trouble actually getting the relations working, so have re-done that code, I think it definetly works now
I have posted the code on my website:-
http://riftor.g615.co.uk/content.php?view=50&type=1
Great piece of code!
Does anyone know if it's possible to to use to_xml and build_from_xml with 3 levels of associations, like:
--------------------------------------
class Level1 < ActiveRecord::Base
has_many :level2s
end
class Level2 < ActiveRecord::Base
belongs_to :level1
has_many :level3s
end
class Level3 < ActiveRecord::Base
belongs_to :level2
End
--------------------------------------
It would be great to be able to move in XML whole pieces of data, whatever structure depth they have.
Cheers,
Phlippe
to_xml does not to 3 levels of association but Wayne's build_from_xml should do (if its associations code is working), and mine certainly does do any depth.
@test.to_xml(:skip_instruct => true, :except => [ :id, :user_id ], :include => { :questions => {:except => [:question_id, :id], :include => [:answers]}, :interpretations => {}})
first level - test
second level - question
third level - answer
I tried to use "build_to_xml" and it restore only one object from each level (((
require "rexml/document"
module ActiveRecord
class Base
def self.build_from_xml(xml)
xml = REXML::Document.new(xml).elements[1] if xml.class == String
ar = self.new
xml.elements.each do | ele |
sym = ele.name.underscore.to_sym
# An association
if ele.has_elements?
klass = self.reflect_on_association(sym).klass
ele.elements.each do | obj |
ar.__send__(sym) << klass.build_from_xml(obj)
end
# An attribute
elsif !ele.text.nil?
ar[sym] = ele.text
end
end
return ar
end
end
end
If not, I'll use your code or Dominic's modified version. But it would be great if this is either included in the Rails base or distributed as a plugin.
Thanks for this anyways.