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

Added serializer namespacing option #879

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
* adds `has_one` attribute for backwards compatibility [@ggordon]
* updates JSON API support to RC3 [@mateomurphy]
* adds fragment cache support [@joaomdmoura]
* adds cache support to attributes and associations [@joaomdmoura]
* adds cache support to attributes and associations [@joaomdmoura]
* adds a `:namespace` option to association and array serializer [@groyoh]
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,16 @@ The `has_many`, `has_one`, and `belongs_to` declarations describe relationships
resources. By default, when you serialize a `Post`, you will get its `Comment`s
as well.

You may use the `:namespace` options to specify the `module` where the serializer(s) of an
association may be found, for example:

```ruby
has_many :comments, namespace: Blogging::Serializers
```
The serializer used then depends on the class of the object to be serialized e.g. if a
comment is an instance of class `AdminComment`, the serializer would use
`Blogging::Serializers::AdminComment` to serializer this comment.

You may also use the `:serializer` option to specify a custom serializer class, for example:

```ruby
Expand All @@ -258,6 +268,20 @@ You may also use the `:serializer` option to specify a custom serializer class,
The `url` declaration describes which named routes to use while generating URLs
for your JSON. Not every adapter will require URLs.

## Array serializer
When calling the `ActiveModel::Serializer::ArraySerializer#new`, you may pass the `:namespace`
to specify the module in which the objects serializers may be found. The serializers used then
depend on the objects class.

Take the example below:

```ruby
a = [Post.new,AdminComment.new]
ActiveRecord::Serializer.ArraySerializer.new(a, namespace: Blogging::Serializers)
```
Here the objects of `a` would respectively be serialized using `Blogging::Serializers::Post`
and `Blogging::Serializers::AdminComment.`

## Caching

To cache a serializer, call ```cache``` and pass its options.
Expand Down
31 changes: 26 additions & 5 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,14 @@ def self.serializer_for(resource, options = {})
if resource.respond_to?(:to_ary)
config.array_serializer
else
options
.fetch(:association_options, {})
.fetch(:serializer, get_serializer_for(resource.class))
resource_class = resource.class
association_options = options.fetch(:association_options, {})
namespace = options[:namespace] || association_options[:namespace]
if namespace
get_namespaced_serializer_for(namespace,resource_class)
else
association_options.fetch(:serializer, get_serializer_for(resource_class))
end
end
end

Expand Down Expand Up @@ -228,8 +233,11 @@ def each_association(&block)

def serializer_from_options(options)
opts = {}
serializer = options.fetch(:association_options, {}).fetch(:serializer, nil)
opts[:serializer] = serializer if serializer
association_options = options.fetch(:association_options, {})
[:serializer,:namespace].each do |key|
value = association_options.fetch(key, nil)
opts[key] = value if value
end
opts
end

Expand All @@ -241,6 +249,19 @@ def self.serializers_cache

attr_reader :options

def self.get_namespaced_serializer_for(namespace, klass)
serializer_class_name = "#{namespace}::#{klass.name.demodulize}"
serializers_cache.fetch_or_store(serializer_class_name) do
serializer_class = serializer_class_name.safe_constantize

if serializer_class
serializer_class
elsif klass.superclass
get_namespaced_serializer_for(namespace, klass.superclass)
end
end
end

def self.get_serializer_for(klass)
serializers_cache.fetch_or_store(klass) do
serializer_class_name = "#{klass.name}Serializer"
Expand Down
3 changes: 1 addition & 2 deletions lib/active_model/serializer/array_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ class ArraySerializer

def initialize(objects, options = {})
options.merge!(root: nil)

@objects = objects.map do |object|
serializer_class = options.fetch(
:serializer,
ActiveModel::Serializer.serializer_for(object)
ActiveModel::Serializer.serializer_for(object, options)
)
serializer_class.new(object, options)
end
Expand Down
12 changes: 12 additions & 0 deletions test/array_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ def test_each_object_should_be_serialized_with_appropriate_serializer

assert_equal serializers.last.custom_options[:some], :options
end

def test_each_object_should_be_serialized_with_appropriate_serializer
@comment = BasicComment.new
@serializer = ArraySerializer.new([@comment, @post], {namespace: Test::Serializer})
serializers = @serializer.to_a

assert_kind_of Test::Serializer::BasicComment, serializers.first
assert_kind_of Comment, serializers.first.object

assert_kind_of Test::Serializer::Post, serializers.last
assert_kind_of Post, serializers.last.object
end
end
end
end
48 changes: 38 additions & 10 deletions test/fixtures/poro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,18 @@ class ProfilePreviewSerializer < ActiveModel::Serializer
urls :posts, :comments
end

Post = Class.new(Model)
Like = Class.new(Model)
Comment = Class.new(Model)
Author = Class.new(Model)
Bio = Class.new(Model)
Blog = Class.new(Model)
Role = Class.new(Model)
User = Class.new(Model)
Location = Class.new(Model)
Place = Class.new(Model)
Post = Class.new(Model)
Like = Class.new(Model)
Comment = Class.new(Model)
BasicComment = Class.new(Comment)
SpecialComment = Class.new(Comment)
Author = Class.new(Model)
Bio = Class.new(Model)
Blog = Class.new(Model)
Role = Class.new(Model)
User = Class.new(Model)
Location = Class.new(Model)
Place = Class.new(Model)

module Spam; end
Spam::UnrelatedLink = Class.new(Model)
Expand Down Expand Up @@ -204,3 +206,29 @@ def self.root_name
Spam::UnrelatedLinkSerializer = Class.new(ActiveModel::Serializer) do
attributes :id
end

module Test
module Serializer
Post = Class.new(ActiveModel::Serializer) do
attributes :id, :title

has_many :comments, namespace: Test::Serializer
end

BasicComment = Class.new(ActiveModel::Serializer) do
attribute :id

belongs_to :post, namespace: Test::Serializer
end

SpecialComment = Class.new(ActiveModel::Serializer) do
attributes :id, :special

belongs_to :post, namespace: Test::Serializer

def special
"I'm so special!"
end
end
end
end
9 changes: 9 additions & 0 deletions test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ def test_serializer_options_are_passed_into_associations_serializers
end
end

def test_association_with_namespace_options_uses_namespace_serializer
@post.comments = [BasicComment.new, SpecialComment.new]
@post_serializer = Test::Serializer::Post.new(@post)
@post_serializer.each_association do |name, serializer, options|
assert_kind_of Test::Serializer::BasicComment, serializer.to_a.first
assert_kind_of Test::Serializer::SpecialComment, serializer.to_a.last
end
end

def test_belongs_to
assert_equal(
{ post: { type: :belongs_to, association_options: {} },
Expand Down
15 changes: 15 additions & 0 deletions test/serializers/serializer_for_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class SerializerTest < Minitest::Test
class MyProfile < Profile
end

class TopComment < BasicComment
end

def setup
@profile = Profile.new
@my_profile = MyProfile.new
Expand All @@ -50,6 +53,18 @@ def test_serializer_inherited_serializer
serializer = ActiveModel::Serializer.serializer_for(@my_profile)
assert_equal ProfileSerializer, serializer
end

def test_serializer_in_module
post = Post.new
serializer = ActiveModel::Serializer.serializer_for(post, { namespace: Test::Serializer })
assert_equal Test::Serializer::Post, serializer
end

def test_serializer_inherited_serializer_in_module
comment = TopComment.new
serializer = ActiveModel::Serializer.serializer_for(comment, { namespace: Test::Serializer })
assert_equal Test::Serializer::BasicComment, serializer
end
end
end
end
Expand Down