diff --git a/README.md b/README.md index 5dfb47315..714b37669 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,12 @@ The `has_many` and `belongs_to` declarations describe relationships between resources. By default, when you serialize a `Post`, you will get its `Comment`s as well. +You may also use the `:serializer` option to specify a custom serializer class, for example: + +```ruby + has_many :comments, serializer: CommentPreviewSerializer +``` + The `url` declaration describes which named routes to use while generating URLs for your JSON. Not every adapter will require URLs. diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index f105ab5ac..d4087fe6b 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -23,7 +23,7 @@ def self.attributes(*attrs) attrs.each do |attr| define_method attr do - object.read_attribute_for_serialization(attr) + object && object.read_attribute_for_serialization(attr) end unless method_defined?(attr) end end @@ -67,7 +67,7 @@ def self.associate(type, attrs) #:nodoc: end end - self._associations[attr] = {type: type, options: options} + self._associations[attr] = {type: type, association_options: options} end end @@ -79,11 +79,13 @@ def self.urls(*attrs) @_urls.concat attrs end - def self.serializer_for(resource) + def self.serializer_for(resource, options = {}) if resource.respond_to?(:to_ary) config.array_serializer else - get_serializer_for(resource.class) + options + .fetch(:association_options, {}) + .fetch(:serializer, get_serializer_for(resource.class)) end end @@ -146,16 +148,27 @@ def attributes(options = {}) def each_association(&block) self.class._associations.dup.each do |name, options| + next unless object association = object.send(name) - serializer_class = ActiveModel::Serializer.serializer_for(association) - serializer = serializer_class.new(association) if serializer_class + serializer_class = ActiveModel::Serializer.serializer_for(association, options) + serializer = serializer_class.new( + association, + serializer_from_options(options) + ) if serializer_class if block_given? - block.call(name, serializer, options[:options]) + block.call(name, serializer, options[:association_options]) end end end + def serializer_from_options(options) + opts = {} + serializer = options.fetch(:options, {}).fetch(:serializer, nil) + opts[:serializer] = serializer if serializer + opts + end + private def self.get_serializer_for(klass) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index d018d5e0e..175d6770c 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -24,7 +24,6 @@ def serializable_hash(options = {}) end else @hash[@root] = attributes_for_serializer(serializer, @options) - add_resource_links(@hash[@root], serializer) end @@ -52,7 +51,7 @@ def add_link(resource, name, serializer) resource[:links] ||= {} resource[:links][name] = nil - if serializer + if serializer && serializer.object type = serialized_object_type(serializer) if name.to_s == type || !type resource[:links][name] = serializer.id.to_s diff --git a/test/adapter/json_api/has_many_explicit_serializer_test.rb b/test/adapter/json_api/has_many_explicit_serializer_test.rb new file mode 100644 index 000000000..72b92494b --- /dev/null +++ b/test/adapter/json_api/has_many_explicit_serializer_test.rb @@ -0,0 +1,65 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class JsonApi + # Test 'has_many :assocs, serializer: AssocXSerializer' + class HasManyExplicitSerializerTest < Minitest::Test + def setup + @post = Post.new(title: 'New Post', body: 'Body') + @author = Author.new(name: 'Jane Blogger') + @author.posts = [@post] + @post.author = @author + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @post.comments = [@first_comment, @second_comment] + @first_comment.post = @post + @first_comment.author = nil + @second_comment.post = @post + @second_comment.author = nil + + @serializer = PostPreviewSerializer.new(@post) + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new( + @serializer, + include: 'comments,author' + ) + end + + def test_includes_comment_ids + assert_equal(['1', '2'], + @adapter.serializable_hash[:posts][:links][:comments]) + end + + def test_includes_linked_comments + assert_equal([{ id: '1', body: "ZOMG A COMMENT", links: { post: @post.id.to_s, author: nil }}, + { id: '2', body: "ZOMG ANOTHER COMMENT", links: { post: @post.id.to_s, author: nil }}], + @adapter.serializable_hash[:linked][:comments]) + end + + def test_includes_author_id + assert_equal(@author.id.to_s, + @adapter.serializable_hash[:posts][:links][:author]) + end + + def test_includes_linked_authors + assert_equal([{ id: @author.id.to_s, links: { posts: [@post.id.to_s] } }], + @adapter.serializable_hash[:linked][:authors]) + end + + def test_explicit_serializer_with_null_resource + @post.author = nil + assert_equal(nil, + @adapter.serializable_hash[:posts][:links][:author]) + end + + def test_explicit_serializer_with_null_collection + @post.comments = [] + assert_equal([], + @adapter.serializable_hash[:posts][:links][:comments]) + end + end + end + end + end +end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index f7c1becb7..6b30387c8 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -100,3 +100,26 @@ def json_key attribute :id attribute :name, key: :title end + +CommentPreviewSerializer = Class.new(ActiveModel::Serializer) do + attributes :id + + belongs_to :post +end + +AuthorPreviewSerializer = Class.new(ActiveModel::Serializer) do + attributes :id + + has_many :posts +end + +PostPreviewSerializer = Class.new(ActiveModel::Serializer) do + def self.root_name + 'posts' + end + + attributes :title, :body, :id + + has_many :comments, serializer: CommentPreviewSerializer + belongs_to :author, serializer: AuthorPreviewSerializer +end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index b2278b450..62a152733 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -42,9 +42,9 @@ def setup def test_has_many assert_equal( - { posts: { type: :has_many, options: { embed: :ids } }, - roles: { type: :has_many, options: { embed: :ids } }, - bio: { type: :belongs_to, options: {} } }, + { posts: { type: :has_many, association_options: { embed: :ids } }, + roles: { type: :has_many, association_options: { embed: :ids } }, + bio: { type: :belongs_to, association_options: {} } }, @author_serializer.class._associations ) @author_serializer.each_association do |name, serializer, options| @@ -64,7 +64,7 @@ def test_has_many end def test_has_one - assert_equal({post: {type: :belongs_to, options: {}}, :author=>{:type=>:belongs_to, :options=>{}}}, @comment_serializer.class._associations) + assert_equal({post: {type: :belongs_to, association_options: {}}, :author=>{:type=>:belongs_to, :association_options=>{}}}, @comment_serializer.class._associations) @comment_serializer.each_association do |name, serializer, options| if name == :post assert_equal({}, options)