Skip to content

Commit fad4ef1

Browse files
committed
Refactor reflection building of association
1 parent cb16457 commit fad4ef1

File tree

2 files changed

+98
-54
lines changed

2 files changed

+98
-54
lines changed

lib/active_model/serializer/reflection.rb

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class Serializer
4747
#
4848
# So you can inspect reflections in your Adapters.
4949
class Reflection < Field
50+
REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze
51+
5052
def initialize(*)
5153
super
5254
options[:links] = {}
@@ -71,8 +73,8 @@ def initialize(*)
7173
# meta ids: ids
7274
# end
7375
# end
74-
def link(name, value = nil, &block)
75-
options[:links][name] = block || value
76+
def link(name, value = nil)
77+
options[:links][name] = block_given? ? Proc.new : value
7678
:nil
7779
end
7880

@@ -86,8 +88,8 @@ def link(name, value = nil, &block)
8688
# href object.blog.id.to_s
8789
# meta(id: object.blog.id)
8890
# end
89-
def meta(value = nil, &block)
90-
options[:meta] = block || value
91+
def meta(value = nil)
92+
options[:meta] = block_given? ? Proc.new : value
9193
:nil
9294
end
9395

@@ -119,23 +121,6 @@ def include_data(value = true)
119121
:nil
120122
end
121123

122-
# @param serializer [ActiveModel::Serializer]
123-
# @yield [ActiveModel::Serializer]
124-
# @return [:nil, associated resource or resource collection]
125-
def value(serializer, include_slice)
126-
@object = serializer.object
127-
@scope = serializer.scope
128-
129-
block_value = instance_exec(serializer, &block) if block
130-
return unless include_data?(include_slice)
131-
132-
if block && block_value != :nil
133-
block_value
134-
else
135-
serializer.read_attribute_for_serialization(name)
136-
end
137-
end
138-
139124
# Build association. This method is used internally to
140125
# build serializer's association by its reflection.
141126
#
@@ -157,43 +142,66 @@ def value(serializer, include_slice)
157142
#
158143
# @api private
159144
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
160-
reflection_options = options.dup
145+
reflection_options = settings.merge(include_data: include_data?(include_slice)) unless block?
146+
association_options = build_association_options(parent_serializer, parent_serializer_options[:namespace], include_slice)
147+
association_value = association_options[:association_value]
148+
serializer_class = association_options[:association_serializer]
161149

162-
# Pass the parent's namespace onto the child serializer
163-
reflection_options[:namespace] ||= parent_serializer_options[:namespace]
164-
165-
association_value = value(parent_serializer, include_slice)
166-
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
167-
reflection_options[:include_data] = include_data?(include_slice)
168-
reflection_options[:links] = options[:links]
169-
reflection_options[:meta] = options[:meta]
150+
reflection_options ||= settings.merge(include_data: include_data?(include_slice)) # Needs to be after association_value is evaluated unless reflection.block.nil?
170151

171152
if serializer_class
172-
serializer = catch(:no_serializer) do
173-
serializer_class.new(
174-
association_value,
175-
serializer_options(parent_serializer, parent_serializer_options, reflection_options)
176-
)
177-
end
178-
if serializer.nil?
179-
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
180-
else
153+
if (serializer = build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class))
181154
reflection_options[:serializer] = serializer
155+
else
156+
# BUG: per #2027, JSON API resource relationships are only id and type, and hence either
157+
# *require* a serializer or we need to be a little clever about figuring out the id/type.
158+
# In either case, returning the raw virtual value will almost always be incorrect.
159+
#
160+
# Should be reflection_options[:virtual_value] or adapter needs to figure out what to do
161+
# with an object that is non-nil and has no defined serializer.
162+
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
182163
end
183164
elsif !association_value.nil? && !association_value.instance_of?(Object)
184165
reflection_options[:virtual_value] = association_value
185166
end
186167

187-
block = nil
188-
Association.new(name, reflection_options, block)
168+
association_block = nil
169+
Association.new(name, reflection_options, association_block)
189170
end
190171

191172
protected
192173

193174
# used in instance exec
194175
attr_accessor :object, :scope
195176

196-
private
177+
def settings
178+
options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) }
179+
end
180+
181+
# Evaluation of the reflection.block will mutate options.
182+
# So, the settings cannot be used until the block is evaluated.
183+
# This means that each time the block is evaluated, it may set a new
184+
# value in the reflection instance. This is not thread-safe.
185+
# @example
186+
# has_many :likes do
187+
# meta liked: object.likes.any?
188+
# include_data: object.loaded?
189+
# end
190+
def block?
191+
!block.nil?
192+
end
193+
194+
def serializer?
195+
options.key?(:serializer)
196+
end
197+
198+
def serializer
199+
options[:serializer]
200+
end
201+
202+
def namespace
203+
options[:namespace]
204+
end
197205

198206
def include_data?(include_slice)
199207
include_data_setting = options[:include_data_setting]
@@ -205,13 +213,49 @@ def include_data?(include_slice)
205213
end
206214
end
207215

208-
def serializer_options(parent_serializer, parent_serializer_options, reflection_options)
209-
serializer = reflection_options.fetch(:serializer, nil)
216+
# @param serializer [ActiveModel::Serializer]
217+
# @yield [ActiveModel::Serializer]
218+
# @return [:nil, associated resource or resource collection]
219+
def value(serializer, include_slice)
220+
@object = serializer.object
221+
@scope = serializer.scope
210222

211-
serializer_options = parent_serializer_options.except(:serializer)
212-
serializer_options[:serializer] = serializer if serializer
213-
serializer_options[:serializer_context_class] = parent_serializer.class
214-
serializer_options
223+
block_value = instance_exec(serializer, &block) if block
224+
return unless include_data?(include_slice)
225+
226+
if block && block_value != :nil
227+
block_value
228+
else
229+
serializer.read_attribute_for_serialization(name)
230+
end
231+
end
232+
233+
def build_association_options(parent_serializer, parent_serializer_namespace_option, include_slice)
234+
serializer_for_options = {
235+
# Pass the parent's namespace onto the child serializer
236+
namespace: namespace || parent_serializer_namespace_option
237+
}
238+
serializer_for_options[:serializer] = serializer if serializer?
239+
association_value = value(parent_serializer, include_slice)
240+
{
241+
association_value: association_value,
242+
association_serializer: parent_serializer.class.serializer_for(association_value, serializer_for_options)
243+
}
244+
end
245+
246+
# NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection
247+
# serializer. This is a good reason for the reflection to have a to_many? or collection? type method.
248+
#
249+
# @return [ActiveModel::Serializer, nil]
250+
def build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)
251+
catch(:no_serializer) do
252+
# Make all the parent serializer instance options available to associations
253+
# except ActiveModelSerializers-specific ones we don't want.
254+
serializer_options = parent_serializer_options.except(:serializer)
255+
serializer_options[:serializer_context_class] = parent_serializer.class
256+
serializer_options[:serializer] = serializer if serializer
257+
serializer_class.new(association_value, serializer_options)
258+
end
215259
end
216260
end
217261
end

test/serializers/reflection_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_reflection_value
5757
assert_equal true, reflection.options.fetch(:include_data_setting)
5858

5959
include_slice = :does_not_matter
60-
assert_equal @model.blog, reflection.value(serializer_instance, include_slice)
60+
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
6161
end
6262

6363
def test_reflection_value_block
@@ -77,7 +77,7 @@ def test_reflection_value_block
7777
assert_equal true, reflection.options.fetch(:include_data_setting)
7878

7979
include_slice = :does_not_matter
80-
assert_equal @model.blog, reflection.value(serializer_instance, include_slice)
80+
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
8181
end
8282

8383
def test_reflection_value_block_with_explicit_include_data_true
@@ -98,7 +98,7 @@ def test_reflection_value_block_with_explicit_include_data_true
9898
assert_equal true, reflection.options.fetch(:include_data_setting)
9999

100100
include_slice = :does_not_matter
101-
assert_equal @model.blog, reflection.value(serializer_instance, include_slice)
101+
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
102102
end
103103

104104
def test_reflection_value_block_with_include_data_false_mutates_the_reflection_include_data
@@ -117,7 +117,7 @@ def test_reflection_value_block_with_include_data_false_mutates_the_reflection_i
117117
assert_respond_to reflection.block, :call
118118
assert_equal true, reflection.options.fetch(:include_data_setting)
119119
include_slice = :does_not_matter
120-
assert_nil reflection.value(serializer_instance, include_slice)
120+
assert_nil reflection.send(:value, serializer_instance, include_slice)
121121
assert_equal false, reflection.options.fetch(:include_data_setting)
122122
end
123123

@@ -137,7 +137,7 @@ def test_reflection_value_block_with_include_data_if_sideloaded_included_mutates
137137
assert_respond_to reflection.block, :call
138138
assert_equal true, reflection.options.fetch(:include_data_setting)
139139
include_slice = {}
140-
assert_nil reflection.value(serializer_instance, include_slice)
140+
assert_nil reflection.send(:value, serializer_instance, include_slice)
141141
assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
142142
end
143143

@@ -157,7 +157,7 @@ def test_reflection_value_block_with_include_data_if_sideloaded_excluded_mutates
157157
assert_respond_to reflection.block, :call
158158
assert_equal true, reflection.options.fetch(:include_data_setting)
159159
include_slice = { blog: :does_not_matter }
160-
assert_equal @model.blog, reflection.value(serializer_instance, include_slice)
160+
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
161161
assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
162162
end
163163

0 commit comments

Comments
 (0)