From 9147469842bf5fd71e919b2d23944c8983214dec Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Fri, 2 Oct 2015 16:18:25 +0200 Subject: [PATCH] Extend serializer lookup to the parent serializer. --- CHANGELOG.md | 1 + docs/general/getting_started.md | 31 +++++++ lib/active_model/serializer.rb | 19 ++++- .../serializer/array_serializer.rb | 5 +- lib/active_model/serializer/reflection.rb | 7 +- test/serializers/associations_test.rb | 82 +++++++++++++++++++ test/serializers/serializer_for_test.rb | 23 ++++++ 7 files changed, 160 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af0c463..d5175369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Breaking changes: 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) - [#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 diff --git a/docs/general/getting_started.md b/docs/general/getting_started.md index 72e84ae7..011f90ce 100644 --- a/docs/general/getting_started.md +++ b/docs/general/getting_started.md @@ -57,6 +57,37 @@ class CommentSerializer < ActiveModel::Serializer 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 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: diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 643167a7..9623de05 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -108,10 +108,25 @@ module ActiveModel Digest::MD5.hexdigest(serializer_file_contents) 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) serializers_cache.fetch_or_store(klass) do - serializer_class_name = "#{klass.name}Serializer" - serializer_class = serializer_class_name.safe_constantize + # NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs. + serializer_class = serializer_lookup_chain_for(klass).map(&:safe_constantize).find { |x| x } if serializer_class serializer_class diff --git a/lib/active_model/serializer/array_serializer.rb b/lib/active_model/serializer/array_serializer.rb index c8029cc7..fe2a6ebb 100644 --- a/lib/active_model/serializer/array_serializer.rb +++ b/lib/active_model/serializer/array_serializer.rb @@ -11,9 +11,8 @@ module ActiveModel @root = options[:root] @object = resources @serializers = resources.map do |resource| - serializer_class = options.fetch(:serializer) do - ActiveModel::Serializer.serializer_for(resource) - end + serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer) + serializer_class = options.fetch(:serializer) { serializer_context_class.serializer_for(resource) } if serializer_class.nil? fail NoSerializerError, "No serializer found for resource: #{resource.inspect}" diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index d9dbd670..e2333303 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -42,13 +42,13 @@ module ActiveModel def build_association(subject, parent_serializer_options) association_value = subject.send(name) 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 begin serializer = serializer_class.new( association_value, - serializer_options(parent_serializer_options, reflection_options) + serializer_options(subject, parent_serializer_options, reflection_options) ) rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError reflection_options[:virtual_value] = association_value.try(:as_json) || association_value @@ -62,11 +62,12 @@ module ActiveModel 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_options = parent_serializer_options.except(:serializer) serializer_options[:serializer] = serializer if serializer + serializer_options[:serializer_context_class] = subject.class serializer_options end end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index a063e060..1748206a 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -125,6 +125,88 @@ module ActiveModel assert expected_association_keys.include? :writer assert expected_association_keys.include? :site 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 diff --git a/test/serializers/serializer_for_test.rb b/test/serializers/serializer_for_test.rb index a0dc0888..7dc18935 100644 --- a/test/serializers/serializer_for_test.rb +++ b/test/serializers/serializer_for_test.rb @@ -27,8 +27,19 @@ module ActiveModel end 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 end + class CustomProfile def serializer_class; ProfileSerializer; end end @@ -59,6 +70,18 @@ module ActiveModel serializer = ActiveModel::Serializer.serializer_for(@custom_profile) assert_equal ProfileSerializer, serializer 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