Extend serializer lookup to the parent serializer.

This commit is contained in:
Lucas Hosseini 2015-10-02 16:18:25 +02:00
parent d02cd30fe5
commit 9147469842
7 changed files with 160 additions and 8 deletions

View File

@ -14,6 +14,7 @@ Breaking changes:
Features: Features:
- [#1225](https://github.com/rails-api/active_model_serializers/pull/1125) Better serializer lookup, use nested serializer when it exists (@beauby)
- [#1172](https://github.com/rails-api/active_model_serializers/pull/1172) Better serializer registration, get more than just the first module (@bf4) - [#1172](https://github.com/rails-api/active_model_serializers/pull/1172) Better serializer registration, get more than just the first module (@bf4)
- [#1158](https://github.com/rails-api/active_model_serializers/pull/1158) Add support for wildcards in `include` option (@beauby) - [#1158](https://github.com/rails-api/active_model_serializers/pull/1158) Add support for wildcards in `include` option (@beauby)
- [#1127](https://github.com/rails-api/active_model_serializers/pull/1127) Add support for nested - [#1127](https://github.com/rails-api/active_model_serializers/pull/1127) Add support for nested

View File

@ -57,6 +57,37 @@ class CommentSerializer < ActiveModel::Serializer
end end
``` ```
### Namespaced Models
When serializing a model inside a namespace, such as `Api::V1::Post`, AMS will expect the corresponding serializer to be inside the same namespace (namely `Api::V1::PostSerializer`).
### Model Associations and Nested Serializers
When declaring a serializer for a model with associations, such as:
```ruby
class PostSerializer < ActiveModel::Serializer
has_many :comments
end
```
AMS will look for `PostSerializer::CommentSerializer` in priority, and fall back to `::CommentSerializer` in case the former does not exist. This allows for more control over the way a model gets serialized as an association of an other model.
For example, in the following situation:
```ruby
class CommentSerializer < ActiveModel::Serializer
attributes :body, :date, :nb_likes
end
class PostSerializer < ActiveModel::Serializer
has_many :comments
class CommentSerializer < ActiveModel::Serializer
attributes :body_short
end
end
```
AMS will use `PostSerializer::CommentSerializer` (thus including only the `:body_short` attribute) when serializing a `Comment` as part of a `Post`, but use `::CommentSerializer` when serializing a `Comment` directly (thus including `:body, :date, :nb_likes`).
## Rails Integration ## Rails Integration
AMS will automatically integrate with you Rails app, you won't need to update your controller, this is a example of how it will look like: AMS will automatically integrate with you Rails app, you won't need to update your controller, this is a example of how it will look like:

View File

@ -108,10 +108,25 @@ module ActiveModel
Digest::MD5.hexdigest(serializer_file_contents) Digest::MD5.hexdigest(serializer_file_contents)
end end
# @api private
def self.serializer_lookup_chain_for(klass)
chain = []
resource_class_name = klass.name.demodulize
resource_namespace = klass.name.deconstantize
serializer_class_name = "#{resource_class_name}Serializer"
chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
chain.push("#{resource_namespace}::#{serializer_class_name}")
chain
end
# @api private
def self.get_serializer_for(klass) def self.get_serializer_for(klass)
serializers_cache.fetch_or_store(klass) do serializers_cache.fetch_or_store(klass) do
serializer_class_name = "#{klass.name}Serializer" # NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
serializer_class = serializer_class_name.safe_constantize serializer_class = serializer_lookup_chain_for(klass).map(&:safe_constantize).find { |x| x }
if serializer_class if serializer_class
serializer_class serializer_class

View File

@ -11,9 +11,8 @@ module ActiveModel
@root = options[:root] @root = options[:root]
@object = resources @object = resources
@serializers = resources.map do |resource| @serializers = resources.map do |resource|
serializer_class = options.fetch(:serializer) do serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer)
ActiveModel::Serializer.serializer_for(resource) serializer_class = options.fetch(:serializer) { serializer_context_class.serializer_for(resource) }
end
if serializer_class.nil? if serializer_class.nil?
fail NoSerializerError, "No serializer found for resource: #{resource.inspect}" fail NoSerializerError, "No serializer found for resource: #{resource.inspect}"

View File

@ -42,13 +42,13 @@ module ActiveModel
def build_association(subject, parent_serializer_options) def build_association(subject, parent_serializer_options)
association_value = subject.send(name) association_value = subject.send(name)
reflection_options = options.dup reflection_options = options.dup
serializer_class = ActiveModel::Serializer.serializer_for(association_value, reflection_options) serializer_class = subject.class.serializer_for(association_value, reflection_options)
if serializer_class if serializer_class
begin begin
serializer = serializer_class.new( serializer = serializer_class.new(
association_value, association_value,
serializer_options(parent_serializer_options, reflection_options) serializer_options(subject, parent_serializer_options, reflection_options)
) )
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
@ -62,11 +62,12 @@ module ActiveModel
private private
def serializer_options(parent_serializer_options, reflection_options) def serializer_options(subject, parent_serializer_options, reflection_options)
serializer = reflection_options.fetch(:serializer, nil) serializer = reflection_options.fetch(:serializer, nil)
serializer_options = parent_serializer_options.except(:serializer) serializer_options = parent_serializer_options.except(:serializer)
serializer_options[:serializer] = serializer if serializer serializer_options[:serializer] = serializer if serializer
serializer_options[:serializer_context_class] = subject.class
serializer_options serializer_options
end end
end end

View File

@ -125,6 +125,88 @@ module ActiveModel
assert expected_association_keys.include? :writer assert expected_association_keys.include? :writer
assert expected_association_keys.include? :site assert expected_association_keys.include? :site
end end
class NamespacedResourcesTest < Minitest::Test
class ResourceNamespace
Post = Class.new(::Model)
Comment = Class.new(::Model)
Author = Class.new(::Model)
Description = Class.new(::Model)
class PostSerializer < ActiveModel::Serializer
has_many :comments
belongs_to :author
has_one :description
end
CommentSerializer = Class.new(ActiveModel::Serializer)
AuthorSerializer = Class.new(ActiveModel::Serializer)
DescriptionSerializer = Class.new(ActiveModel::Serializer)
end
def setup
@comment = ResourceNamespace::Comment.new
@author = ResourceNamespace::Author.new
@description = ResourceNamespace::Description.new
@post = ResourceNamespace::Post.new(comments: [@comment],
author: @author,
description: @description)
@post_serializer = ResourceNamespace::PostSerializer.new(@post)
end
def test_associations_namespaced_resources
@post_serializer.associations.each do |association|
case association.key
when :comments
assert_instance_of(ResourceNamespace::CommentSerializer, association.serializer.first)
when :author
assert_instance_of(ResourceNamespace::AuthorSerializer, association.serializer)
when :description
assert_instance_of(ResourceNamespace::DescriptionSerializer, association.serializer)
else
flunk "Unknown association: #{key}"
end
end
end
end
class NestedSerializersTest < Minitest::Test
Post = Class.new(::Model)
Comment = Class.new(::Model)
Author = Class.new(::Model)
Description = Class.new(::Model)
class PostSerializer < ActiveModel::Serializer
has_many :comments
CommentSerializer = Class.new(ActiveModel::Serializer)
belongs_to :author
AuthorSerializer = Class.new(ActiveModel::Serializer)
has_one :description
DescriptionSerializer = Class.new(ActiveModel::Serializer)
end
def setup
@comment = Comment.new
@author = Author.new
@description = Description.new
@post = Post.new(comments: [@comment],
author: @author,
description: @description)
@post_serializer = PostSerializer.new(@post)
end
def test_associations_namespaced_resources
@post_serializer.associations.each do |association|
case association.key
when :comments
assert_instance_of(PostSerializer::CommentSerializer, association.serializer.first)
when :author
assert_instance_of(PostSerializer::AuthorSerializer, association.serializer)
when :description
assert_instance_of(PostSerializer::DescriptionSerializer, association.serializer)
else
flunk "Unknown association: #{key}"
end
end
end
end
end end
end end
end end

View File

@ -27,8 +27,19 @@ module ActiveModel
end end
class SerializerTest < Minitest::Test class SerializerTest < Minitest::Test
module ResourceNamespace
Post = Class.new(::Model)
Comment = Class.new(::Model)
class PostSerializer < ActiveModel::Serializer
class CommentSerializer < ActiveModel::Serializer
end
end
end
class MyProfile < Profile class MyProfile < Profile
end end
class CustomProfile class CustomProfile
def serializer_class; ProfileSerializer; end def serializer_class; ProfileSerializer; end
end end
@ -59,6 +70,18 @@ module ActiveModel
serializer = ActiveModel::Serializer.serializer_for(@custom_profile) serializer = ActiveModel::Serializer.serializer_for(@custom_profile)
assert_equal ProfileSerializer, serializer assert_equal ProfileSerializer, serializer
end end
def test_serializer_for_namespaced_resource
post = ResourceNamespace::Post.new
serializer = ActiveModel::Serializer.serializer_for(post)
assert_equal(ResourceNamespace::PostSerializer, serializer)
end
def test_serializer_for_nested_resource
comment = ResourceNamespace::Comment.new
serializer = ResourceNamespace::PostSerializer.serializer_for(comment)
assert_equal(ResourceNamespace::PostSerializer::CommentSerializer, serializer)
end
end end
end end
end end