Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rendering array objects that doesn't have serializers #962

Merged
merged 8 commits into from
Jun 26, 2015
15 changes: 8 additions & 7 deletions lib/action_controller/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,18 @@ def use_adapter?
options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }

if use_adapter? && (serializer = get_serializer(resource))

@_serializer_opts[:scope] ||= serialization_scope
@_serializer_opts[:scope_name] = _serialization_scope

# omg hax
object = serializer.new(resource, @_serializer_opts)
adapter = ActiveModel::Serializer::Adapter.create(object, @_adapter_opts)
super(adapter, options)
else
super(resource, options)
begin
serialized = serializer.new(resource, @_serializer_opts)
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add some docs on building your own array serializer as described in https://github.com/rails-api/active_model_serializers#2-for-an-array-resource and the use of this exception class at some point. Want me to make an issue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on a quest to keep the readme as clean as possible, but I agree that this can be helpful for some ppl, like others details that are poping up.
I'm thinking about start the AMS wiki (using gh wiki feature). For now, make yourself comfortable to open the issue to make sure we won't forget it while I think it trough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like wikis, but then it's another thing to garden. For AMS I think it could be a plus.

else
resource = ActiveModel::Serializer::Adapter.create(serialized, @_adapter_opts)
end
end

super(resource, options)
end
end

Expand Down
14 changes: 10 additions & 4 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,16 @@ def each_association(&block)
serializer_class = ActiveModel::Serializer.serializer_for(association_value, association_options)

if serializer_class
serializer = serializer_class.new(
association_value,
options.except(:serializer).merge(serializer_from_options(association_options))
)
begin
serializer = serializer_class.new(
association_value,
options.except(:serializer).merge(serializer_from_options(association_options))
)
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
virtual_value = association_value
virtual_value = virtual_value.as_json if virtual_value.respond_to?(:as_json)
association_options[:association_options][:virtual_value] = virtual_value
end
elsif !association_value.nil? && !association_value.instance_of?(Object)
association_options[:association_options][:virtual_value] = association_value
end
Expand Down
8 changes: 7 additions & 1 deletion lib/active_model/serializer/array_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module ActiveModel
class Serializer
class ArraySerializer
NoSerializerError = Class.new(StandardError)
include Enumerable
delegate :each, to: :@objects

Expand All @@ -13,7 +14,12 @@ def initialize(objects, options = {})
:serializer,
ActiveModel::Serializer.serializer_for(object)
)
serializer_class.new(object, options.except(:serializer))

if serializer_class.nil?
fail NoSerializerError, "No serializer found for object: #{object.inspect}"
else
serializer_class.new(object, options.except(:serializer))
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having serializer_for return nil for if any elements don't have an associated serializer? That would maintain current PR behavior, I think, might make more sense SRP-wise, but might be a performance hit.

Should we add a test for a collection or association that contains an element with a known serializer and another with a nil serializer class? I'm really not sure how to handle that in AMS without more complexity. I'd rather push that back to the end user, but that could be laziness.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to update this PR if we change this behaviour at serializer_for.

I think it's an edge-case. I haven't heard about ppl using AMS to renders arrays that mix serializable and non-serializable objects yet, so let's not worry about it for now 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

winning

end
@meta = options[:meta]
@meta_key = options[:meta_key]
Expand Down
22 changes: 22 additions & 0 deletions test/action_controller/serialization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ def render_object_with_cache_enabled
render json: @post
end

def render_json_object_without_serializer
render json: {error: 'Result is Invalid'}
end

def render_json_array_object_without_serializer
render json: [{error: 'Result is Invalid'}]
end

def update_and_render_object_with_cache_enabled
@post.updated_at = DateTime.now

Expand Down Expand Up @@ -160,6 +168,20 @@ def test_render_using_default_root
assert_equal expected.to_json, @response.body
end

def test_render_json_object_without_serializer
get :render_json_object_without_serializer

assert_equal 'application/json', @response.content_type
assert_equal ({error: 'Result is Invalid'}).to_json, @response.body
end

def test_render_json_array_object_without_serializer
get :render_json_array_object_without_serializer

assert_equal 'application/json', @response.content_type
assert_equal ([{error: 'Result is Invalid'}]).to_json, @response.body
end

def test_render_array_using_implicit_serializer
get :render_array_using_implicit_serializer
assert_equal 'application/json', @response.content_type
Expand Down
23 changes: 17 additions & 6 deletions test/adapter/json/has_many_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class HasManyTestTest < Minitest::Test
def setup
ActionController::Base.cache_store.clear
@author = Author.new(id: 1, name: 'Steve K.')
@post = Post.new(title: 'New Post', body: 'Body')
@post = Post.new(id: 42, title: 'New Post', body: 'Body')
@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]
Expand All @@ -17,20 +17,31 @@ def setup
@second_comment.post = @post
@blog = Blog.new(id: 1, name: "My Blog!!")
@post.blog = @blog

@serializer = PostSerializer.new(@post)
@adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer)
@tag = Tag.new(id: 1, name: "#hash_tag")
@post.tags = [@tag]
end

def test_has_many
serializer = PostSerializer.new(@post)
adapter = ActiveModel::Serializer::Adapter::Json.new(serializer)
assert_equal([
{id: 1, body: 'ZOMG A COMMENT'},
{id: 2, body: 'ZOMG ANOTHER COMMENT'}
], @adapter.serializable_hash[:post][:comments])
], adapter.serializable_hash[:post][:comments])
end

def test_has_many_with_no_serializer
serializer = PostWithTagsSerializer.new(@post)
adapter = ActiveModel::Serializer::Adapter::Json.new(serializer)
assert_equal({
id: 42,
tags: [
{"attributes"=>{"id"=>1, "name"=>"#hash_tag"}}
]
}.to_json, adapter.serializable_hash[:post_with_tags].to_json)
end
end
end
end
end
end

19 changes: 18 additions & 1 deletion test/adapter/json_api/has_many_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def setup
@blog.articles = [@post]
@post.blog = @blog
@post_without_comments.blog = nil

@tag = Tag.new(id: 1, name: "#hash_tag")
@post.tags = [@tag]
@serializer = PostSerializer.new(@post)
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer)
end
Expand Down Expand Up @@ -95,6 +96,7 @@ def test_include_type_for_association_when_different_than_name
serializer = BlogSerializer.new(@blog)
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer)
actual = adapter.serializable_hash[:data][:relationships][:articles]

expected = {
data: [{
type: "posts",
Expand All @@ -103,6 +105,21 @@ def test_include_type_for_association_when_different_than_name
}
assert_equal expected, actual
end

def test_has_many_with_no_serializer
serializer = PostWithTagsSerializer.new(@post)
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer)

assert_equal({
data: {
id: "1",
type: "posts",
relationships: {
tags: { data: nil }
}
}
}, adapter.serializable_hash)
end
end
end
end
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/poro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class ProfilePreviewSerializer < ActiveModel::Serializer
User = Class.new(Model)
Location = Class.new(Model)
Place = Class.new(Model)
Tag = Class.new(Model)
Comment = Class.new(Model) do
# Uses a custom non-time-based cache key
def cache_key
Expand Down Expand Up @@ -224,6 +225,12 @@ def self.root_name
belongs_to :author, serializer: AuthorPreviewSerializer
end

PostWithTagsSerializer = Class.new(ActiveModel::Serializer) do
attributes :id

has_many :tags
end

Spam::UnrelatedLinkSerializer = Class.new(ActiveModel::Serializer) do
attributes :id
end
Expand Down
10 changes: 10 additions & 0 deletions test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ def setup
@author.roles = []
@blog = Blog.new({ name: 'AMS Blog' })
@post = Post.new({ title: 'New Post', body: 'Body' })
@tag = Tag.new({name: '#hashtagged'})
@comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' })
@post.comments = [@comment]
@post.tags = [@tag]
@post.blog = @blog
@comment.post = @post
@comment.author = nil
Expand Down Expand Up @@ -65,6 +67,14 @@ def test_has_many_and_has_one
end
end

def test_has_many_with_no_serializer
PostWithTagsSerializer.new(@post).each_association do |name, serializer, options|
assert_equal name, :tags
assert_equal serializer, nil
assert_equal [{ attributes: { name: "#hashtagged" }}].to_json, options[:virtual_value].to_json
end
end

def test_serializer_options_are_passed_into_associations_serializers
@post_serializer.each_association do |name, association|
if name == :comments
Expand Down