aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorJeremy Kemper <jeremy@bitsweat.net>2006-06-03 00:01:08 +0000
committerJeremy Kemper <jeremy@bitsweat.net>2006-06-03 00:01:08 +0000
commit48052d70ec065a3a8d9e6e121cab5ae857f8da1a (patch)
treed007080f5222dbfc585f705c9046d72e3447c4d2 /activerecord
parent49060cda24d40d13bb428b0a1a4f3d35ef3e3c23 (diff)
downloadrails-48052d70ec065a3a8d9e6e121cab5ae857f8da1a.tar.gz
rails-48052d70ec065a3a8d9e6e121cab5ae857f8da1a.tar.bz2
rails-48052d70ec065a3a8d9e6e121cab5ae857f8da1a.zip
to_xml fixes, features, and speedup. Closes #4989.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4413 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG2
-rwxr-xr-xactiverecord/lib/active_record/base.rb210
-rwxr-xr-xactiverecord/test/base_test.rb46
-rw-r--r--activerecord/test/fixtures/topics.yml4
4 files changed, 210 insertions, 52 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 9f0e71aad4..fa55eb147d 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Allow models to override to_xml. #4989 [Blair Zajac <blair@orcaware.com>]
+
* PostgreSQL: don't ignore port when host is nil since it's often used to label the domain socket. #5247 [shimbo@is.naist.jp]
* Records and arrays of records are bound as quoted ids. [Jeremy Kemper]
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 735c8203aa..0a81681126 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,3 +1,4 @@
+require 'base64'
require 'yaml'
require 'set'
require 'active_record/deprecated_finders'
@@ -84,7 +85,7 @@ module ActiveRecord #:nodoc:
# The array form is to be used when the condition input is tainted and requires sanitization. The string form can
# be used for statements that don't involve tainted data. Examples:
#
- # User < ActiveRecord::Base
+ # class User < ActiveRecord::Base
# def self.authenticate_unsafely(user_name, password)
# find(:first, :conditions => "user_name = '#{user_name}' AND password = '#{password}'")
# end
@@ -1024,7 +1025,7 @@ module ActiveRecord #:nodoc:
safe_to_array(first) + safe_to_array(second)
end
- # Object#to_a is deprecated, though it does have the desired behaviour
+ # Object#to_a is deprecated, though it does have the desired behavior
def safe_to_array(o)
case o
when NilClass
@@ -1635,7 +1636,7 @@ module ActiveRecord #:nodoc:
# Builds an XML document to represent the model. Some configuration is
# availble through +options+, however more complicated cases should use
- # Builder.
+ # override ActiveRecord's to_xml.
#
# By default the generated XML document will include the processing
# instruction and all object's attributes. For example:
@@ -1655,8 +1656,14 @@ module ActiveRecord #:nodoc:
# <last-read type="date">2004-04-15</last-read>
# </topic>
#
- # This behaviour can be controlled with :only, :except, and :skip_instruct
- # for instance:
+ # This behavior can be controlled with :only, :except,
+ # :skip_instruct, :skip_types and :dasherize. The :only and
+ # :except options are the same as for the #attributes method.
+ # The default is to dasherize all column names, to disable this,
+ # set :dasherize to false. To not have the column type included
+ # in the XML output, set :skip_types to false.
+ #
+ # For instance:
#
# topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
#
@@ -1697,49 +1704,198 @@ module ActiveRecord #:nodoc:
#
# To include any methods on the object(s) being called use :methods
#
- # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
+ # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
#
# <firm>
# # ... normal attributes as shown above ...
# <calculated-earnings>100000000000000000</calculated-earnings>
# <real-earnings>5</real-earnings>
# </firm>
+ #
+ # To call any Proc's on the object(s) use :procs. The Proc's
+ # are passed a modified version of the options hash that was
+ # given to #to_xml.
+ #
+ # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
+ # firm.to_xml :procs => [ proc ]
+ #
+ # <firm>
+ # # ... normal attributes as shown above ...
+ # <abc>def</abc>
+ # </firm>
+ #
+ # You may override the to_xml method in your ActiveRecord::Base
+ # subclasses if you need to. The general form of doing this is
+ #
+ # class IHaveMyOwnXML < ActiveRecord::Base
+ # def to_xml(options = {})
+ # options[:indent] ||= 2
+ # xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ # xml.instruct! unless options[:skip_instruct]
+ # xml.level_one do
+ # xml.tag!(:second_level, 'content')
+ # end
+ # end
+ # end
def to_xml(options = {})
- options[:root] ||= self.class.to_s.underscore
- options[:except] = Array(options[:except]) << self.class.inheritance_column unless options[:only] # skip type column
- root_only_or_except = { :only => options[:only], :except => options[:except] }
+ options[:indent] ||= 2
+ builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
- attributes_for_xml = attributes(root_only_or_except)
-
- if methods_to_call = options.delete(:methods)
- [*methods_to_call].each do |meth|
- attributes_for_xml[meth] = send(meth)
- end
+ unless options[:skip_instruct]
+ builder.instruct!
+ options[:skip_instruct] = true
end
-
- if include_associations = options.delete(:include)
- include_has_options = include_associations.is_a?(Hash)
-
- for association in include_has_options ? include_associations.keys : Array(include_associations)
- association_options = include_has_options ? include_associations[association] : root_only_or_except
- case self.class.reflect_on_association(association).macro
+ root = (options[:root] || self.class.to_s.underscore).to_s
+ if dasherize = !options.has_key?(:dasherize) || options[:dasherize]
+ root = root.dasherize
+ end
+
+ builder.tag!(root) do
+ # To replicate the behavior in ActiveRecord#attributes,
+ # :except takes precedence over :only. If :only is not set
+ # for a N level model but is set for the N+1 level models,
+ # then because :except is set to a default value, the second
+ # level model can have both :except and :only set. So if
+ # :only is set, always delete :except.
+
+ a_names = self.attribute_names
+ if options[:only]
+ options.delete(:except)
+ a_names = a_names & Array(options[:only]).collect { |n| n.to_s }
+ else
+ options[:except] = Array(options[:except]) | Array(self.class.inheritance_column)
+ a_names = a_names - options[:except].collect { |n| n.to_s }
+ end
+
+ columns_hash = self.class.columns_hash
+ a_names.each do |name|
+
+ # To be consistent with the ActiveRecord instance being
+ # converted into a Hash using #attributes, convert SQL
+ # type names. Map the different "string" types all to
+ # "string", as the client of the XML does not care if a
+ # TEXT or VARCHAR column stores the value.
+
+ type = columns_hash[name].type
+ case type
+ when :text
+ type = :string
+ when :time
+ type = :datetime
+ end
+
+ attributes = options[:skip_types] ? { } : { :type => type }
+
+ value = self.send(name)
+ if dasherize
+ name = name.dasherize
+ end
+
+ # There is a significant speed improvement if the value
+ # does not need to be escaped, as #tag! escapes all values
+ # to ensure that valid XML is generated. For known binary
+ # values, it is at least an order of magnitude faster to
+ # Base64 encode binary values and directly put them in the
+ # output XML than to pass the original value or the Base64
+ # encoded value to the #tag! method. It definitely makes
+ # no sense to Base64 encode the value and then give it to
+ # #tag!, since that just adds additional overhead.
+ if value.nil?
+ attributes[:nil] = 'true'
+ builder.tag!(name, attributes)
+ else
+ value_needs_no_encoding = false
+ case type
+ when :binary
+ value = Base64.encode64(value)
+ attributes[:encoding] = 'base64'
+ value_needs_no_encoding = true
+ when :date
+ value = value.to_s(:db)
+ value_needs_no_encoding = true
+ when :datetime
+ value = value.xmlschema
+ value_needs_no_encoding = true
+ when :boolean, :float, :integer
+ value = value.to_s
+ value_needs_no_encoding = true
+ end
+
+ if value_needs_no_encoding
+ builder.tag!(name, attributes) do
+ builder << value
+ end
+ else
+ builder.tag!(name, value, attributes)
+ end
+ end
+ end
+
+ if methods_to_call = options.delete(:methods)
+ [ *methods_to_call ].each do |meth|
+ value = self.send(meth)
+
+ tag = dasherize ? meth.to_s.dasherize : meth.to_s
+
+ type_name = ActiveSupport::CoreExtensions::Hash::Conversions::XML_TYPE_NAMES[value.class]
+
+ if formatter = ActiveSupport::CoreExtensions::Hash::Conversions::XML_FORMATTING[type_name]
+ value = formatter.call(value)
+ end
+
+ if value.nil?
+ attributes = { :nil => true }
+ else
+ if !options[:skip_types] && !type_name.nil?
+ attributes = { :type => type_name }
+ else
+ attributes = { }
+ end
+ end
+
+ builder.tag!(tag, value, attributes)
+ end
+ end
+
+ if include_associations = options.delete(:include)
+ root_only_or_except = { :except => options[:except],
+ :only => options[:only] }
+
+ include_has_options = include_associations.is_a?(Hash)
+
+ for association in include_has_options ? include_associations.keys : Array(include_associations)
+ association_options = include_has_options ? include_associations[association] : root_only_or_except
+
+ opts = options.merge(association_options)
+
+ case self.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
records = send(association).to_a
unless records.empty?
- attributes_for_xml[association] = records.collect do |record|
- record.attributes(association_options)
+ tag = records.first.class.to_s.underscore.pluralize
+ if dasherize
+ tag = tag.dasherize
+ end
+ builder.tag!(tag) do
+ records.each { |r| r.to_xml(opts) }
end
end
when :has_one, :belongs_to
if record = send(association)
- attributes_for_xml[association] = record.attributes(association_options)
+ record.to_xml(opts.merge(:root => association))
end
+ end
end
end
- end
- attributes_for_xml.to_xml(options)
+ if procs = options.delete(:procs)
+ [ *procs ].each do |proc|
+ proc.call(options)
+ end
+ end
+
+ end
end
private
diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb
index 6ea46d8e85..5a6870faa3 100755
--- a/activerecord/test/base_test.rb
+++ b/activerecord/test/base_test.rb
@@ -439,18 +439,18 @@ class BasicsTest < Test::Unit::TestCase
def test_increment_counter
Topic.increment_counter("replies_count", 1)
- assert_equal 1, Topic.find(1).replies_count
+ assert_equal 2, Topic.find(1).replies_count
Topic.increment_counter("replies_count", 1)
- assert_equal 2, Topic.find(1).replies_count
+ assert_equal 3, Topic.find(1).replies_count
end
def test_decrement_counter
Topic.decrement_counter("replies_count", 2)
- assert_equal 1, Topic.find(2).replies_count
+ assert_equal -1, Topic.find(2).replies_count
Topic.decrement_counter("replies_count", 2)
- assert_equal 0, Topic.find(1).replies_count
+ assert_equal -2, Topic.find(2).replies_count
end
def test_update_all
@@ -987,12 +987,12 @@ class BasicsTest < Test::Unit::TestCase
end
def test_increment_attribute
- assert_equal 0, topics(:first).replies_count
+ assert_equal 1, topics(:first).replies_count
topics(:first).increment! :replies_count
- assert_equal 1, topics(:first, :reload).replies_count
+ assert_equal 2, topics(:first, :reload).replies_count
topics(:first).increment(:replies_count).increment!(:replies_count)
- assert_equal 3, topics(:first, :reload).replies_count
+ assert_equal 4, topics(:first, :reload).replies_count
end
def test_increment_nil_attribute
@@ -1003,13 +1003,13 @@ class BasicsTest < Test::Unit::TestCase
def test_decrement_attribute
topics(:first).increment(:replies_count).increment!(:replies_count)
- assert_equal 2, topics(:first).replies_count
+ assert_equal 3, topics(:first).replies_count
topics(:first).decrement!(:replies_count)
- assert_equal 1, topics(:first, :reload).replies_count
+ assert_equal 2, topics(:first, :reload).replies_count
topics(:first).decrement(:replies_count).decrement!(:replies_count)
- assert_equal -1, topics(:first, :reload).replies_count
+ assert_equal 0, topics(:first, :reload).replies_count
end
def test_toggle_attribute
@@ -1217,14 +1217,14 @@ class BasicsTest < Test::Unit::TestCase
written_on_in_current_timezone = topics(:first).written_on.xmlschema
last_read_in_current_timezone = topics(:first).last_read.xmlschema
assert_equal "<topic>", xml.first(7)
- assert xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
+ assert xml.include?(%(<title type="string">The First Topic</title>))
+ assert xml.include?(%(<author-name type="string">David</author-name>))
assert xml.include?(%(<id type="integer">1</id>))
- assert xml.include?(%(<replies-count type="integer">0</replies-count>))
+ assert xml.include?(%(<replies-count type="integer">1</replies-count>))
assert xml.include?(%(<written-on type="datetime">#{written_on_in_current_timezone}</written-on>))
- assert xml.include?(%(<content>Have a nice day</content>))
- assert xml.include?(%(<author-email-address>david@loudthinking.com</author-email-address>))
- assert xml.include?(%(<parent-id></parent-id>))
+ assert xml.include?(%(<content type="string">Have a nice day</content>))
+ assert xml.include?(%(<author-email-address type="string">david@loudthinking.com</author-email-address>))
+ assert xml.match(%r{<parent-id (type="integer"\s*|nil="true"\s*){2}/>})
if current_adapter?(:SybaseAdapter) or current_adapter?(:SQLServerAdapter)
assert xml.include?(%(<last-read type="datetime">#{last_read_in_current_timezone}</last-read>))
else
@@ -1236,23 +1236,23 @@ class BasicsTest < Test::Unit::TestCase
assert xml.include?(%(<bonus-time type="datetime">#{bonus_time_in_current_timezone}</bonus-time>))
end
end
-
+
def test_to_xml_skipping_attributes
xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => :title)
assert_equal "<topic>", xml.first(7)
- assert !xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
+ assert !xml.include?(%(<title type="string">The First Topic</title>))
+ assert xml.include?(%(<author-name type="string">David</author-name>))
xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [ :title, :author_name ])
- assert !xml.include?(%(<title>The First Topic</title>))
- assert !xml.include?(%(<author-name>David</author-name>))
+ assert !xml.include?(%(<title type="string">The First Topic</title>))
+ assert !xml.include?(%(<author-name type="string">David</author-name>))
end
def test_to_xml_including_has_many_association
xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
assert_equal "<topic>", xml.first(7)
assert xml.include?(%(<replies><reply>))
- assert xml.include?(%(<title>The Second Topic's of the day</title>))
+ assert xml.include?(%(<title type="string">The Second Topic's of the day</title>))
end
def test_to_xml_including_belongs_to_association
@@ -1277,7 +1277,7 @@ class BasicsTest < Test::Unit::TestCase
)
assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<client><name>Summit</name></client>))
+ assert xml.include?(%(<client><name type="string">Summit</name></client>))
assert xml.include?(%(<clients><client>))
end
diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml
index 810bbcf4e8..b1e5159be2 100644
--- a/activerecord/test/fixtures/topics.yml
+++ b/activerecord/test/fixtures/topics.yml
@@ -8,7 +8,7 @@ first:
bonus_time: 2005-01-30t15:28:00.00+01:00
content: Have a nice day
approved: false
- replies_count: 0
+ replies_count: 1
second:
id: 2
@@ -17,6 +17,6 @@ second:
written_on: 2003-07-15t15:28:00.00+01:00
content: Have a nice day
approved: true
- replies_count: 2
+ replies_count: 0
parent_id: 1
type: Reply