aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb33
-rw-r--r--activemodel/lib/active_model/serialization.rb46
-rw-r--r--activemodel/lib/active_model/serializers/xml.rb40
-rw-r--r--activemodel/test/cases/serialization_test.rb88
-rw-r--r--activemodel/test/cases/serializers/xml_serialization_test.rb52
5 files changed, 236 insertions, 23 deletions
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 6ee5e04267..bdc0eb4a0d 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -314,6 +314,7 @@ module ActiveModel
end
end
end
+ attribute_method_matchers_cache.clear
end
# Removes all the previously dynamically defined methods from the class
@@ -321,6 +322,7 @@ module ActiveModel
generated_attribute_methods.module_eval do
instance_methods.each { |m| undef_method(m) }
end
+ attribute_method_matchers_cache.clear
end
# Returns true if the attribute methods defined have been generated.
@@ -338,6 +340,29 @@ module ActiveModel
end
private
+ # The methods +method_missing+ and +respond_to?+ of this module are
+ # invoked often in a typical rails, both of which invoke the method
+ # +match_attribute_method?+. The latter method iterates through an
+ # array doing regular expression matches, which results in a lot of
+ # object creations. Most of the times it returns a +nil+ match. As the
+ # match result is always the same given a +method_name+, this cache is
+ # used to alleviate the GC, which ultimately also speeds up the app
+ # significantly (in our case our test suite finishes 10% faster with
+ # this cache).
+ def attribute_method_matchers_cache
+ @attribute_method_matchers_cache ||= {}
+ end
+
+ def attribute_method_matcher(method_name)
+ if attribute_method_matchers_cache.key?(method_name)
+ attribute_method_matchers_cache[method_name]
+ else
+ match = nil
+ attribute_method_matchers.detect { |method| match = method.match(method_name) }
+ attribute_method_matchers_cache[method_name] = match
+ end
+ end
+
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
@@ -411,12 +436,8 @@ module ActiveModel
# Returns a struct representing the matching attribute method.
# The struct's attributes are prefix, base and suffix.
def match_attribute_method?(method_name)
- self.class.attribute_method_matchers.each do |method|
- if (match = method.match(method_name)) && attribute_method?(match.attr_name)
- return match
- end
- end
- nil
+ match = self.class.send(:attribute_method_matcher, method_name)
+ match && attribute_method?(match.attr_name) ? match : nil
end
# prevent method_missing from calling private methods with #send
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 4a174cb62a..9260c5082d 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -69,18 +69,46 @@ module ActiveModel
def serializable_hash(options = nil)
options ||= {}
- only = Array.wrap(options[:only]).map(&:to_s)
- except = Array.wrap(options[:except]).map(&:to_s)
-
attribute_names = attributes.keys.sort
- if only.any?
- attribute_names &= only
- elsif except.any?
- attribute_names -= except
+ if only = options[:only]
+ attribute_names &= Array.wrap(only).map(&:to_s)
+ elsif except = options[:except]
+ attribute_names -= Array.wrap(except).map(&:to_s)
+ end
+
+ method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
+ hash = Hash[(attribute_names + method_names).map { |n| [n, send(n)] }]
+
+ serializable_add_includes(options) do |association, records, opts|
+ hash[association] = if records.is_a?(Enumerable)
+ records.map { |a| a.serializable_hash(opts) }
+ else
+ records.serializable_hash(opts)
+ end
end
- method_names = Array.wrap(options[:methods]).map { |n| n if respond_to?(n.to_s) }.compact
- Hash[(attribute_names + method_names).map { |n| [n, send(n)] }]
+ hash
end
+
+ private
+ # Add associations specified via the <tt>:include</tt> option.
+ #
+ # Expects a block that takes as arguments:
+ # +association+ - name of the association
+ # +records+ - the association record(s) to be serialized
+ # +opts+ - options for the association records
+ def serializable_add_includes(options = {})
+ return unless include = options[:include]
+
+ unless include.is_a?(Hash)
+ include = Hash[Array.wrap(include).map { |n| [n, {}] }]
+ end
+
+ include.each do |association, opts|
+ if records = send(association)
+ yield association, records, opts
+ end
+ end
+ end
end
end
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
index 9812af43d6..64dda3bcee 100644
--- a/activemodel/lib/active_model/serializers/xml.rb
+++ b/activemodel/lib/active_model/serializers/xml.rb
@@ -101,6 +101,7 @@ module ActiveModel
@builder.tag!(*args) do
add_attributes_and_methods
+ add_includes
add_extra_behavior
add_procs
yield @builder if block_given?
@@ -120,6 +121,45 @@ module ActiveModel
end
end
+ def add_includes
+ @serializable.send(:serializable_add_includes, options) do |association, records, opts|
+ add_associations(association, records, opts)
+ end
+ end
+
+ # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
+ def add_associations(association, records, opts)
+ merged_options = opts.merge(options.slice(:builder, :indent))
+ merged_options[:skip_instruct] = true
+
+ if records.is_a?(Enumerable)
+ tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
+ type = options[:skip_types] ? { } : {:type => "array"}
+ association_name = association.to_s.singularize
+ merged_options[:root] = association_name
+
+ if records.empty?
+ @builder.tag!(tag, type)
+ else
+ @builder.tag!(tag, type) do
+ records.each do |record|
+ if options[:skip_types]
+ record_type = {}
+ else
+ record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
+ record_type = {:type => record_class}
+ end
+
+ record.to_xml merged_options.merge(record_type)
+ end
+ end
+ end
+ else
+ merged_options[:root] = association.to_s
+ records.to_xml(merged_options)
+ end
+ end
+
def add_procs
if procs = options.delete(:procs)
Array.wrap(procs).each do |proc|
diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb
index 8cc1ccb1e7..5122f08eec 100644
--- a/activemodel/test/cases/serialization_test.rb
+++ b/activemodel/test/cases/serialization_test.rb
@@ -1,13 +1,19 @@
require "cases/helper"
+require 'active_support/core_ext/object/instance_variables'
class SerializationTest < ActiveModel::TestCase
class User
include ActiveModel::Serialization
- attr_accessor :name, :email, :gender
+ attr_accessor :name, :email, :gender, :address, :friends
+
+ def initialize(name, email, gender)
+ @name, @email, @gender = name, email, gender
+ @friends = []
+ end
def attributes
- @attributes ||= {'name' => 'nil', 'email' => 'nil', 'gender' => 'nil'}
+ instance_values.except("address", "friends")
end
def foo
@@ -15,11 +21,25 @@ class SerializationTest < ActiveModel::TestCase
end
end
+ class Address
+ include ActiveModel::Serialization
+
+ attr_accessor :street, :city, :state, :zip
+
+ def attributes
+ instance_values
+ end
+ end
+
setup do
- @user = User.new
- @user.name = 'David'
- @user.email = 'david@example.com'
- @user.gender = 'male'
+ @user = User.new('David', 'david@example.com', 'male')
+ @user.address = Address.new
+ @user.address.street = "123 Lane"
+ @user.address.city = "Springfield"
+ @user.address.state = "CA"
+ @user.address.zip = 11111
+ @user.friends = [User.new('Joe', 'joe@example.com', 'male'),
+ User.new('Sue', 'sue@example.com', 'female')]
end
def test_method_serializable_hash_should_work
@@ -42,4 +62,60 @@ class SerializationTest < ActiveModel::TestCase
assert_equal expected , @user.serializable_hash(:methods => [:foo])
end
+ def test_method_serializable_hash_should_work_with_only_and_methods
+ expected = {:foo=>"i_am_foo"}
+ assert_equal expected , @user.serializable_hash(:only => [], :methods => [:foo])
+ end
+
+ def test_method_serializable_hash_should_work_with_except_and_methods
+ expected = {"gender"=>"male", :foo=>"i_am_foo"}
+ assert_equal expected , @user.serializable_hash(:except => [:name, :email], :methods => [:foo])
+ end
+
+ def test_should_not_call_methods_that_dont_respond
+ expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"}
+ assert_equal expected , @user.serializable_hash(:methods => [:bar])
+ end
+
+ def test_include_option_with_singular_association
+ expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com",
+ :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}}
+ assert_equal expected , @user.serializable_hash(:include => :address)
+ end
+
+ def test_include_option_with_plural_association
+ expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
+ :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
+ {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
+ assert_equal expected , @user.serializable_hash(:include => :friends)
+ end
+
+ def test_include_option_with_empty_association
+ @user.friends = []
+ expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", :friends=>[]}
+ assert_equal expected , @user.serializable_hash(:include => :friends)
+ end
+
+ def test_multiple_includes
+ expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
+ :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111},
+ :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
+ {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
+ assert_equal expected , @user.serializable_hash(:include => [:address, :friends])
+ end
+
+ def test_include_with_options
+ expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
+ :address=>{"street"=>"123 Lane"}}
+ assert_equal expected , @user.serializable_hash(:include => {:address => {:only => "street"}})
+ end
+
+ def test_nested_include
+ @user.friends.first.friends = [@user]
+ expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
+ :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male',
+ :friends => ["email"=>"david@example.com", "gender"=>"male", "name"=>"David"]},
+ {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', :friends => []}]}
+ assert_equal expected , @user.serializable_hash(:include => {:friends => {:include => :friends}})
+ end
end
diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb
index f978191d22..a38ef8e223 100644
--- a/activemodel/test/cases/serializers/xml_serialization_test.rb
+++ b/activemodel/test/cases/serializers/xml_serialization_test.rb
@@ -7,9 +7,11 @@ class Contact
extend ActiveModel::Naming
include ActiveModel::Serializers::Xml
+ attr_accessor :address, :friends
+
def attributes
- instance_values
- end unless method_defined?(:attributes)
+ instance_values.except("address", "friends")
+ end
end
module Admin
@@ -20,6 +22,17 @@ end
class Customer < Struct.new(:name)
end
+class Address
+ extend ActiveModel::Naming
+ include ActiveModel::Serializers::Xml
+
+ attr_accessor :street, :city, :state, :zip
+
+ def attributes
+ instance_values
+ end
+end
+
class XmlSerializationTest < ActiveModel::TestCase
def setup
@contact = Contact.new
@@ -30,6 +43,12 @@ class XmlSerializationTest < ActiveModel::TestCase
customer = Customer.new
customer.name = "John"
@contact.preferences = customer
+ @contact.address = Address.new
+ @contact.address.street = "123 Lane"
+ @contact.address.city = "Springfield"
+ @contact.address.state = "CA"
+ @contact.address.zip = 11111
+ @contact.friends = [Contact.new, Contact.new]
end
test "should serialize default root" do
@@ -138,4 +157,33 @@ class XmlSerializationTest < ActiveModel::TestCase
assert_match %r{<contact type="Contact">}, xml
assert_match %r{<name>aaron stack</name>}, xml
end
+
+ test "include option with singular association" do
+ xml = @contact.to_xml :include => :address, :indent => 0
+ assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true))
+ end
+
+ test "include option with plural association" do
+ xml = @contact.to_xml :include => :friends, :indent => 0
+ assert_match %r{<friends type="array">}, xml
+ assert_match %r{<friend type="Contact">}, xml
+ end
+
+ test "multiple includes" do
+ xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => [ :address, :friends ]
+ assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true))
+ assert_match %r{<friends type="array">}, xml
+ assert_match %r{<friend type="Contact">}, xml
+ end
+
+ test "include with options" do
+ xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => { :address => { :only => :city } }
+ assert xml.include?(%(><address><city>Springfield</city></address>))
+ end
+
+ test "propagates skip_types option to included associations" do
+ xml = @contact.to_xml :include => :friends, :indent => 0, :skip_types => true
+ assert_match %r{<friends>}, xml
+ assert_match %r{<friend>}, xml
+ end
end