Skip to content

Commit 6018ef1

Browse files
committed
Merge pull request #1225 from beauby/nested-serializer-lookup
Add support for nested serializers
2 parents f52ade2 + 9147469 commit 6018ef1

File tree

7 files changed

+160
-8
lines changed

7 files changed

+160
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Breaking changes:
1414

1515
Features:
1616

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

docs/general/getting_started.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,37 @@ class CommentSerializer < ActiveModel::Serializer
5757
end
5858
```
5959

60+
### Namespaced Models
61+
62+
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`).
63+
64+
### Model Associations and Nested Serializers
65+
66+
When declaring a serializer for a model with associations, such as:
67+
```ruby
68+
class PostSerializer < ActiveModel::Serializer
69+
has_many :comments
70+
end
71+
```
72+
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.
73+
74+
For example, in the following situation:
75+
76+
```ruby
77+
class CommentSerializer < ActiveModel::Serializer
78+
attributes :body, :date, :nb_likes
79+
end
80+
81+
class PostSerializer < ActiveModel::Serializer
82+
has_many :comments
83+
class CommentSerializer < ActiveModel::Serializer
84+
attributes :body_short
85+
end
86+
end
87+
```
88+
89+
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`).
90+
6091
## Rails Integration
6192

6293
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:

lib/active_model/serializer.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,25 @@ def self.digest_caller_file(caller_line)
111111
Digest::MD5.hexdigest(serializer_file_contents)
112112
end
113113

114+
# @api private
115+
def self.serializer_lookup_chain_for(klass)
116+
chain = []
117+
118+
resource_class_name = klass.name.demodulize
119+
resource_namespace = klass.name.deconstantize
120+
serializer_class_name = "#{resource_class_name}Serializer"
121+
122+
chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
123+
chain.push("#{resource_namespace}::#{serializer_class_name}")
124+
125+
chain
126+
end
127+
128+
# @api private
114129
def self.get_serializer_for(klass)
115130
serializers_cache.fetch_or_store(klass) do
116-
serializer_class_name = "#{klass.name}Serializer"
117-
serializer_class = serializer_class_name.safe_constantize
131+
# NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
132+
serializer_class = serializer_lookup_chain_for(klass).map(&:safe_constantize).find { |x| x }
118133

119134
if serializer_class
120135
serializer_class

lib/active_model/serializer/array_serializer.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ def initialize(resources, options = {})
1111
@root = options[:root]
1212
@object = resources
1313
@serializers = resources.map do |resource|
14-
serializer_class = options.fetch(:serializer) do
15-
ActiveModel::Serializer.serializer_for(resource)
16-
end
14+
serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer)
15+
serializer_class = options.fetch(:serializer) { serializer_context_class.serializer_for(resource) }
1716

1817
if serializer_class.nil?
1918
fail NoSerializerError, "No serializer found for resource: #{resource.inspect}"

lib/active_model/serializer/reflection.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ class Serializer
4242
def build_association(subject, parent_serializer_options)
4343
association_value = subject.send(name)
4444
reflection_options = options.dup
45-
serializer_class = ActiveModel::Serializer.serializer_for(association_value, reflection_options)
45+
serializer_class = subject.class.serializer_for(association_value, reflection_options)
4646

4747
if serializer_class
4848
begin
4949
serializer = serializer_class.new(
5050
association_value,
51-
serializer_options(parent_serializer_options, reflection_options)
51+
serializer_options(subject, parent_serializer_options, reflection_options)
5252
)
5353
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
5454
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
@@ -62,11 +62,12 @@ def build_association(subject, parent_serializer_options)
6262

6363
private
6464

65-
def serializer_options(parent_serializer_options, reflection_options)
65+
def serializer_options(subject, parent_serializer_options, reflection_options)
6666
serializer = reflection_options.fetch(:serializer, nil)
6767

6868
serializer_options = parent_serializer_options.except(:serializer)
6969
serializer_options[:serializer] = serializer if serializer
70+
serializer_options[:serializer_context_class] = subject.class
7071
serializer_options
7172
end
7273
end

test/serializers/associations_test.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,88 @@ def test_associations_custom_keys
125125
assert expected_association_keys.include? :writer
126126
assert expected_association_keys.include? :site
127127
end
128+
129+
class NamespacedResourcesTest < Minitest::Test
130+
class ResourceNamespace
131+
Post = Class.new(::Model)
132+
Comment = Class.new(::Model)
133+
Author = Class.new(::Model)
134+
Description = Class.new(::Model)
135+
class PostSerializer < ActiveModel::Serializer
136+
has_many :comments
137+
belongs_to :author
138+
has_one :description
139+
end
140+
CommentSerializer = Class.new(ActiveModel::Serializer)
141+
AuthorSerializer = Class.new(ActiveModel::Serializer)
142+
DescriptionSerializer = Class.new(ActiveModel::Serializer)
143+
end
144+
145+
def setup
146+
@comment = ResourceNamespace::Comment.new
147+
@author = ResourceNamespace::Author.new
148+
@description = ResourceNamespace::Description.new
149+
@post = ResourceNamespace::Post.new(comments: [@comment],
150+
author: @author,
151+
description: @description)
152+
@post_serializer = ResourceNamespace::PostSerializer.new(@post)
153+
end
154+
155+
def test_associations_namespaced_resources
156+
@post_serializer.associations.each do |association|
157+
case association.key
158+
when :comments
159+
assert_instance_of(ResourceNamespace::CommentSerializer, association.serializer.first)
160+
when :author
161+
assert_instance_of(ResourceNamespace::AuthorSerializer, association.serializer)
162+
when :description
163+
assert_instance_of(ResourceNamespace::DescriptionSerializer, association.serializer)
164+
else
165+
flunk "Unknown association: #{key}"
166+
end
167+
end
168+
end
169+
end
170+
171+
class NestedSerializersTest < Minitest::Test
172+
Post = Class.new(::Model)
173+
Comment = Class.new(::Model)
174+
Author = Class.new(::Model)
175+
Description = Class.new(::Model)
176+
class PostSerializer < ActiveModel::Serializer
177+
has_many :comments
178+
CommentSerializer = Class.new(ActiveModel::Serializer)
179+
belongs_to :author
180+
AuthorSerializer = Class.new(ActiveModel::Serializer)
181+
has_one :description
182+
DescriptionSerializer = Class.new(ActiveModel::Serializer)
183+
end
184+
185+
def setup
186+
@comment = Comment.new
187+
@author = Author.new
188+
@description = Description.new
189+
@post = Post.new(comments: [@comment],
190+
author: @author,
191+
description: @description)
192+
@post_serializer = PostSerializer.new(@post)
193+
end
194+
195+
def test_associations_namespaced_resources
196+
@post_serializer.associations.each do |association|
197+
case association.key
198+
when :comments
199+
assert_instance_of(PostSerializer::CommentSerializer, association.serializer.first)
200+
when :author
201+
assert_instance_of(PostSerializer::AuthorSerializer, association.serializer)
202+
when :description
203+
assert_instance_of(PostSerializer::DescriptionSerializer, association.serializer)
204+
else
205+
flunk "Unknown association: #{key}"
206+
end
207+
end
208+
end
209+
end
128210
end
129211
end
130212
end

test/serializers/serializer_for_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,19 @@ def test_overwritten_serializer_for_array
2727
end
2828

2929
class SerializerTest < Minitest::Test
30+
module ResourceNamespace
31+
Post = Class.new(::Model)
32+
Comment = Class.new(::Model)
33+
34+
class PostSerializer < ActiveModel::Serializer
35+
class CommentSerializer < ActiveModel::Serializer
36+
end
37+
end
38+
end
39+
3040
class MyProfile < Profile
3141
end
42+
3243
class CustomProfile
3344
def serializer_class; ProfileSerializer; end
3445
end
@@ -59,6 +70,18 @@ def test_serializer_custom_serializer
5970
serializer = ActiveModel::Serializer.serializer_for(@custom_profile)
6071
assert_equal ProfileSerializer, serializer
6172
end
73+
74+
def test_serializer_for_namespaced_resource
75+
post = ResourceNamespace::Post.new
76+
serializer = ActiveModel::Serializer.serializer_for(post)
77+
assert_equal(ResourceNamespace::PostSerializer, serializer)
78+
end
79+
80+
def test_serializer_for_nested_resource
81+
comment = ResourceNamespace::Comment.new
82+
serializer = ResourceNamespace::PostSerializer.serializer_for(comment)
83+
assert_equal(ResourceNamespace::PostSerializer::CommentSerializer, serializer)
84+
end
6285
end
6386
end
6487
end

0 commit comments

Comments
 (0)