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 this case, +association_ids+ will only include the ids of the comments provided by the
+comments+ method. +comments+ method.
h3. Authorization Scope
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
By default, the authorization scope for serializers is +:current_user+. This means By default, the authorization scope for serializers is +:current_user+. This means
that when you call +render json: @post+, the controller will automatically call 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.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
gem.name = "active_model_serializers" gem.name = "active_model_serializers"
gem.require_paths = ["lib"] gem.require_paths = ["lib"]
gem.version = "0.0.1" gem.version = "0.1.0"
gem.add_dependency 'activemodel', '~> 3.0' gem.add_dependency 'activemodel', '~> 3.0'
gem.add_development_dependency "rails", "~> 3.0" gem.add_development_dependency "rails", "~> 3.0"
end end

View File

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

View File

@ -1,3 +1,5 @@
require "active_support"
require "active_support/core_ext/string/inflections"
require "active_model" require "active_model"
require "active_model/serializer" require "active_model/serializer"
@ -5,14 +7,19 @@ module ActiveModel::SerializerSupport
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods #:nodoc: module ClassMethods #:nodoc:
def active_model_serializer if "".respond_to?(:safe_constantize)
return @active_model_serializer if defined?(@active_model_serializer) 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
begin @active_model_serializer = "#{self.name}Serializer".constantize
@active_model_serializer = "#{self.name}Serializer".constantize rescue NameError => e
rescue NameError => e raise unless e.message =~ /uninitialized constant/
raise unless e.message =~ /uninitialized constant$/ && e.name.to_s == "#{self.name}Serializer" end
end end
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) } { :title => @comment.read_attribute_for_serialization(:title) }
end end
def as_json(*) def as_json(options=nil)
{ :comment => serializable_hash } options ||= {}
if options[:root] == false
serializable_hash
else
{ :comment => serializable_hash }
end
end end
end end
@ -189,65 +194,17 @@ class SerializerTest < ActiveModel::TestCase
}, json) }, json)
end 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 def test_overridden_associations
author_serializer = Class.new(ActiveModel::Serializer) do author_serializer = Class.new(ActiveModel::Serializer) do
attributes :first_name attributes :first_name
end end
blog_serializer = Class.new(ActiveModel::Serializer) do blog_serializer = Class.new(ActiveModel::Serializer) do
const_set(:PersonSerializer, author_serializer)
def person def person
object.author object.author
end end
has_one :person has_one :person, :serializer => author_serializer
end end
user = User.new user = User.new
@ -703,8 +660,7 @@ class SerializerTest < ActiveModel::TestCase
serializer = polymorphic_serializer.new(blog, user) serializer = polymorphic_serializer.new(blog, user)
assert_equal({ assert_equal({
:writer => { :polymorphic_user => {
:writer_type => 'PolymorphicUser',
:first_name => "Jose", :first_name => "Jose",
:last_name => "Valim" :last_name => "Valim"
} }
@ -728,10 +684,7 @@ class SerializerTest < ActiveModel::TestCase
serializer = polymorphic_serializer.new(blog, user) serializer = polymorphic_serializer.new(blog, user)
assert_equal({ assert_equal({
:writer => { :polymorphic_user => 1
:writer_type => 'PolymorphicUser',
:id => 1
}
}, serializer.as_json) }, serializer.as_json)
end end
@ -757,7 +710,19 @@ class SerializerTest < ActiveModel::TestCase
author = author_class.new(:id => 5, :name => "Tom Dale") author = author_class.new(:id => 5, :name => "Tom Dale")
post.author = author 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({ assert_equal({
:blog_post => { :blog_post => {
@ -765,7 +730,13 @@ class SerializerTest < ActiveModel::TestCase
:body => "It's a new post!", :body => "It's a new post!",
:author => { :id => 5, :name => "Tom Dale" } :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 end
def test_serializer_has_access_to_root_object def test_serializer_has_access_to_root_object