Skip to content

Commit d913dbf

Browse files
committed
Extend serializer lookup to the parent serializer and the resource's namespace.
1 parent d02cd30 commit d913dbf

File tree

6 files changed

+128
-8
lines changed

6 files changed

+128
-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, get namespaced resources and nested serializers inferred (@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

lib/active_model/serializer.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,24 @@ def self.digest_caller_file(caller_line)
108108
Digest::MD5.hexdigest(serializer_file_contents)
109109
end
110110

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

116130
if serializer_class
117131
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)