Improve polymorphic associations

This commit is contained in:
adman65 2011-12-21 20:25:40 +02:00
commit da0c33f53c
6 changed files with 99 additions and 130 deletions

View File

@ -456,36 +456,7 @@ The +association_ids+ helper will use the overridden version of the association,
this case, +association_ids+ will only include the ids of the comments provided by the
+comments+ method.
h3. Special Association Serializers
So far, associations defined in serializers use either the +as_json+ method on the model
or the defined serializer for the association type. Sometimes, you may want to serialize
associated models differently when they are requested as part of another resource than
when they are requested on their own.
For instance, we might want to provide the full comment when it is requested directly,
but only its title when requested as part of the post. To achieve this, you can define
a serializer for associated objects nested inside the main serializer.
<pre lang="ruby">
class PostSerializer < ActiveModel::Serializer
class CommentSerializer < ActiveModel::Serializer
attributes :id, :title
end
# same as before
# ...
end
</pre>
In other words, if a +PostSerializer+ is trying to serialize comments, it will first
look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+
and finally +comment.as_json+.
h3. Overriding the Defaults
h4. Authorization Scope
h3. Authorization Scope
By default, the authorization scope for serializers is +:current_user+. This means
that when you call +render json: @post+, the controller will automatically call

View File

@ -11,9 +11,8 @@ Gem::Specification.new do |gem|
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
gem.name = "active_model_serializers"
gem.require_paths = ["lib"]
gem.version = "0.0.1"
gem.version = "0.1.0"
gem.add_dependency 'activemodel', '~> 3.0'
gem.add_development_dependency "rails", "~> 3.0"
end

View File

@ -1,7 +1,5 @@
require "active_support/core_ext/class/attribute"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/module/anonymous"
require "set"
module ActiveModel
# Active Model Array Serializer
@ -84,51 +82,64 @@ module ActiveModel
def polymorphic?
options[:polymorphic]
end
protected
def find_serializable(object, scope, context, options)
if serializer
serializer.new(object, scope, options)
elsif object.respond_to?(:active_model_serializer) && (ams = object.active_model_serializer)
ams.new(object, scope, options)
else
object
end
end
end
class HasMany < Config #:nodoc:
def serialize(collection, scope, options)
collection.map do |item|
serializer.new(item, scope, options).serializable_hash
def serialize(collection, scope, context, options)
array = collection.map do |item|
find_serializable(item, scope, context, options).as_json(:root => false)
end
{ key => array }
end
def serialize_ids(collection, scope)
# use named scopes if they are present
# Use pluck or select_columns if available
# return collection.ids if collection.respond_to?(:ids)
collection.map do |item|
array = collection.map do |item|
item.read_attribute_for_serialization(:id)
end
{ key => array }
end
end
class HasOne < Config #:nodoc:
def serialize(object, scope, options)
return unless object
def serialize(object, scope, context, options)
if polymorphic?
polymorphic_type = object.class.to_s.demodulize
serializer_class = "#{object.class.to_s}Serializer".constantize
serializer_class.new(object, scope, options).serializable_hash.merge({
"#{name}_type".to_sym => polymorphic_type
})
if object
find_serializable(object, scope, context, options).as_json(:root => object.class.to_s.demodulize.underscore.to_sym)
else
{}
end
else
serializer.new(object, scope, options).serializable_hash
{ key => object && find_serializable(object, scope, context, options).as_json(:root => false) }
end
end
def serialize_ids(object, scope)
return unless object
if polymorphic?
{
:id => object.read_attribute_for_serialization(:id),
"#{name}_type".to_sym => object.class.to_s.demodulize
object.class.to_s.demodulize.underscore.to_sym => object.read_attribute_for_serialization(:id),
}
elsif object
{ key => object.read_attribute_for_serialization(:id) }
else
object.read_attribute_for_serialization(:id)
{ key => nil }
end
end
end
@ -165,12 +176,6 @@ module ActiveModel
unless method_defined?(attr)
class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__
end
options[:serializer] ||= options[:polymorphic] || begin
serializer_class = (options[:key] || attr).to_s.classify
const_get("#{serializer_class}Serializer")
end
klass.new(attr, options)
end
end
@ -225,6 +230,9 @@ module ActiveModel
# methods, provided by default by ActiveRecord. You can implement these
# methods on your custom models if you want the serializer's schema method
# to work.
#
# TODO: This is currently coupled to Active Record. We need to
# figure out a way to decouple those two.
def schema
klass = model_class
columns = klass.columns_hash
@ -265,7 +273,6 @@ module ActiveModel
def inherited(klass) #:nodoc:
return if klass.anonymous?
name = klass.name.demodulize.underscore.sub(/_serializer$/, '')
klass.class_eval do
@ -284,8 +291,9 @@ module ActiveModel
# Returns a json representation of the serializable
# object including the root.
def as_json(*)
if root = @options[:root] || _root
def as_json(options=nil)
options ||= {}
if root = options.fetch(:root, @options.fetch(:root, _root))
@hash = hash = {}
hash.merge!(root => serializable_hash)
hash
@ -307,6 +315,8 @@ module ActiveModel
end
end
# Merge associations for embed case by always adding
# root associations to the given hash.
def merge_associations(hash, associations)
associations.each do |key, value|
if hash[key]
@ -324,7 +334,7 @@ module ActiveModel
_associations.each do |association|
associated_object = send(association.name)
hash[association.key] = association.serialize(associated_object, scope, :hash => @hash)
hash.merge! association.serialize(associated_object, scope, self, :hash => @hash)
end
hash
@ -337,7 +347,7 @@ module ActiveModel
_associations.each do |association|
associated_object = send(association.name)
hash[association.key] = association.serialize_ids(associated_object, scope)
hash.merge! association.serialize_ids(associated_object, scope)
end
hash

View File

@ -1,3 +1,5 @@
require "active_support"
require "active_support/core_ext/string/inflections"
require "active_model"
require "active_model/serializer"
@ -5,14 +7,19 @@ module ActiveModel::SerializerSupport
extend ActiveSupport::Concern
module ClassMethods #:nodoc:
def active_model_serializer
return @active_model_serializer if defined?(@active_model_serializer)
if "".respond_to?(:safe_constantize)
def active_model_serializer
@active_model_serializer ||= "#{self.name}Serializer".safe_constantize
end
else
def active_model_serializer
return @active_model_serializer if defined?(@active_model_serializer)
# Use safe constantize when Rails 3.2 is out
begin
@active_model_serializer = "#{self.name}Serializer".constantize
rescue NameError => e
raise unless e.message =~ /uninitialized constant$/ && e.name.to_s == "#{self.name}Serializer"
begin
@active_model_serializer = "#{self.name}Serializer".constantize
rescue NameError => e
raise unless e.message =~ /uninitialized constant/
end
end
end
end

View File

@ -0,0 +1,11 @@
require "test_helper"
class RandomModel
include ActiveModel::SerializerSupport
end
class SerializerSupportTest < ActiveModel::TestCase
test "it returns nil if no serializer exists" do
assert_equal nil, RandomModel.new.active_model_serializer
end
end

View File

@ -78,8 +78,13 @@ class SerializerTest < ActiveModel::TestCase
{ :title => @comment.read_attribute_for_serialization(:title) }
end
def as_json(*)
{ :comment => serializable_hash }
def as_json(options=nil)
options ||= {}
if options[:root] == false
serializable_hash
else
{ :comment => serializable_hash }
end
end
end
@ -189,65 +194,17 @@ class SerializerTest < ActiveModel::TestCase
}, json)
end
def test_implicit_serializer
author_serializer = Class.new(ActiveModel::Serializer) do
attributes :first_name
end
blog_serializer = Class.new(ActiveModel::Serializer) do
const_set(:AuthorSerializer, author_serializer)
has_one :author
end
user = User.new
blog = Blog.new
blog.author = user
json = blog_serializer.new(blog, user).as_json
assert_equal({
:author => {
:first_name => "Jose"
}
}, json)
end
def test_implicit_serializer_for_has_many
blog_with_posts = Class.new(Blog) do
attr_accessor :posts
end
blog_serializer = Class.new(ActiveModel::Serializer) do
const_set(:PostSerializer, PostSerializer)
has_many :posts
end
user = User.new
blog = blog_with_posts.new
blog.posts = [Post.new(:title => 'test')]
json = blog_serializer.new(blog, user).as_json
assert_equal({
:posts => [{
:title => "test",
:body => nil,
:comments => []
}]
}, json)
end
def test_overridden_associations
author_serializer = Class.new(ActiveModel::Serializer) do
attributes :first_name
end
blog_serializer = Class.new(ActiveModel::Serializer) do
const_set(:PersonSerializer, author_serializer)
def person
object.author
end
has_one :person
has_one :person, :serializer => author_serializer
end
user = User.new
@ -703,8 +660,7 @@ class SerializerTest < ActiveModel::TestCase
serializer = polymorphic_serializer.new(blog, user)
assert_equal({
:writer => {
:writer_type => 'PolymorphicUser',
:polymorphic_user => {
:first_name => "Jose",
:last_name => "Valim"
}
@ -728,10 +684,7 @@ class SerializerTest < ActiveModel::TestCase
serializer = polymorphic_serializer.new(blog, user)
assert_equal({
:writer => {
:writer_type => 'PolymorphicUser',
:id => 1
}
:polymorphic_user => 1
}, serializer.as_json)
end
@ -757,7 +710,19 @@ class SerializerTest < ActiveModel::TestCase
author = author_class.new(:id => 5, :name => "Tom Dale")
post.author = author
hash = serializer_class.new(post, nil, :root => :blog_post)
assert_equal({
:blog_post => {
:title => "New Post",
:body => "It's a new post!",
:author => { :id => 5, :name => "Tom Dale" }
}
}, serializer_class.new(post, nil, :root => :blog_post).as_json)
assert_equal({
:title => "New Post",
:body => "It's a new post!",
:author => { :id => 5, :name => "Tom Dale" }
}, serializer_class.new(post, nil, :root => false).as_json)
assert_equal({
:blog_post => {
@ -765,7 +730,13 @@ class SerializerTest < ActiveModel::TestCase
:body => "It's a new post!",
:author => { :id => 5, :name => "Tom Dale" }
}
}, hash.as_json)
}, serializer_class.new(post, nil).as_json(:root => :blog_post))
assert_equal({
:title => "New Post",
:body => "It's a new post!",
:author => { :id => 5, :name => "Tom Dale" }
}, serializer_class.new(post, nil).as_json(:root => false))
end
def test_serializer_has_access_to_root_object