提交 4860143e 编写于 作者: J John Firebaugh

ActiveModel support for the :include serialization option

This commit moves support for the :include serialization option for
serializing associated objects out of ActiveRecord in into ActiveModel.
The following methods support the :include option:

  * serializable_hash
  * to_json
  * to_xml

Instances must respond to methods named by the values of the :includes
array (or keys of the :includes hash). If an association method returns
an object that is_a?(Enumerable) (which AR has_many associations do), it
is assumed to be a collection association, and its elements must respond
to :serializable_hash. Otherwise it must respond to :serializable_hash
itself.

While here, fix #858, XmlSerializer should not singularize already
singular association names.
上级 1723a7a6
......@@ -77,7 +77,38 @@ def serializable_hash(options = nil)
end
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
Hash[(attribute_names + method_names).map { |n| [n, send(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
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
......@@ -101,6 +101,7 @@ def serialize
@builder.tag!(*args) do
add_attributes_and_methods
add_includes
add_extra_behavior
add_procs
yield @builder if block_given?
......@@ -120,6 +121,45 @@ def add_attributes_and_methods
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|
......
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 @@ def foo
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
......@@ -57,4 +77,45 @@ def test_should_not_call_methods_that_dont_respond
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
......@@ -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 @@ class Contact < ::Contact
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 @@ def setup
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 @@ def setup
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
......@@ -10,46 +10,8 @@ def serializable_hash(options = nil)
options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
options[:except] |= Array.wrap(self.class.inheritance_column)
hash = super(options)
serializable_add_includes(options) do |association, records, opts|
hash[association] = records.is_a?(Enumerable) ?
records.map { |r| r.serializable_hash(opts) } :
records.serializable_hash(opts)
end
hash
super(options)
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_associations = options.delete(:include)
include_has_options = include_associations.is_a?(Hash)
associations = include_has_options ? include_associations.keys : Array.wrap(include_associations)
associations.each do |association|
records = case self.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
send(association).to_a
when :has_one, :belongs_to
send(association)
end
if records
association_options = include_has_options ? include_associations[association] : {}
yield(association, records, association_options)
end
end
options[:include] = include_associations
end
end
end
......
......@@ -182,48 +182,6 @@ def initialize(*args)
options[:except] |= Array.wrap(@serializable.class.inheritance_column)
end
def add_extra_behavior
add_includes
end
def add_includes
procs = options.delete(:procs)
@serializable.send(:serializable_add_includes, options) do |association, records, opts|
add_associations(association, records, opts)
end
options[:procs] = procs
end
# TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
def add_associations(association, records, opts)
association_name = association.to_s.singularize
merged_options = options.merge(opts).merge!(:root => association_name, :skip_instruct => true)
if records.is_a?(Enumerable)
tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
type = options[:skip_types] ? { } : {:type => "array"}
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
records.to_xml(merged_options)
end
end
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册