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.
This commit is contained in:
parent
1723a7a6c6
commit
4860143ee4
@ -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|
|
||||
|
@ -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 @@ 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
|
||||
|
Loading…
Reference in New Issue
Block a user