Skip to content

Commit ff5ab21

Browse files
committed
Make Association totally lazy
1 parent 7697d9f commit ff5ab21

File tree

6 files changed

+182
-151
lines changed

6 files changed

+182
-151
lines changed

lib/active_model/serializer/association.rb

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,42 @@ module ActiveModel
44
class Serializer
55
# This class holds all information about serializer's association.
66
#
7-
# @attr [Symbol] name
8-
# @attr [Hash{Symbol => Object}] options
9-
# @attr [block]
10-
#
11-
# @example
12-
# Association.new(:comments, { serializer: CommentSummarySerializer })
13-
#
14-
class Association < Field
7+
# @api private
8+
Association = Struct.new(:reflection, :association_options) do
159
attr_reader :lazy_association
16-
delegate :include_data?, :virtual_value, to: :lazy_association
10+
delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association
1711

1812
def initialize(*)
1913
super
20-
@lazy_association = LazyAssociation.new(name, options, block)
14+
@lazy_association = LazyAssociation.new(reflection, association_options)
2115
end
2216

17+
# @return [Symbol]
18+
delegate :name, to: :reflection
19+
2320
# @return [Symbol]
2421
def key
25-
options.fetch(:key, name)
22+
reflection_options.fetch(:key, name)
2623
end
2724

2825
# @return [True,False]
2926
def key?
30-
options.key?(:key)
27+
reflection_options.key?(:key)
3128
end
3229

3330
# @return [Hash]
3431
def links
35-
options.fetch(:links) || {}
32+
reflection_options.fetch(:links) || {}
3633
end
3734

3835
# @return [Hash, nil]
36+
# This gets mutated, so cannot use the cached reflection_options
3937
def meta
40-
options[:meta]
38+
reflection.options[:meta]
4139
end
4240

4341
def polymorphic?
44-
true == options[:polymorphic]
42+
true == reflection_options[:polymorphic]
4543
end
4644

4745
# @api private
@@ -63,7 +61,7 @@ def serializable_hash(adapter_options, adapter_instance)
6361

6462
private
6563

66-
delegate :reflection, to: :lazy_association
64+
delegate :reflection_options, to: :lazy_association
6765
end
6866
end
6967
end

lib/active_model/serializer/lazy_association.rb

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,107 @@
11
module ActiveModel
22
class Serializer
3-
class LazyAssociation < Field
3+
# @api private
4+
LazyAssociation = Struct.new(:reflection, :association_options) do
5+
REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze
46

5-
def serializer
6-
options[:serializer]
7+
delegate :collection?, to: :reflection
8+
9+
def reflection_options
10+
@reflection_options ||= reflection.options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) }
711
end
812

13+
def object
14+
@object ||= reflection.value(
15+
association_options.fetch(:parent_serializer),
16+
association_options.fetch(:include_slice)
17+
)
18+
end
19+
alias_method :eval_reflection_block, :object
20+
921
def include_data?
10-
options[:include_data]
22+
eval_reflection_block if reflection.block
23+
reflection.include_data?(
24+
association_options.fetch(:include_slice)
25+
)
26+
end
27+
28+
# @return [ActiveModel::Serializer, nil]
29+
def serializer
30+
return @serializer if defined?(@serializer)
31+
if serializer_class
32+
serialize_object!(object)
33+
elsif !object.nil? && !object.instance_of?(Object)
34+
cached_result[:virtual_value] = object
35+
end
36+
@serializer = cached_result[:serializer]
1137
end
1238

1339
def virtual_value
14-
options[:virtual_value]
40+
cached_result[:virtual_value] || reflection_options[:virtual_value]
41+
end
42+
43+
# NOTE(BF): Kurko writes:
44+
# 1. This class is doing a lot more than it should. It has business logic (key/meta/links) and
45+
# it also looks like a factory (serializer/serialize_object/instantiate_serializer/serializer_class).
46+
# It's hard to maintain classes that you can understand what it's really meant to be doing,
47+
# so it ends up having all sorts of methods.
48+
# Perhaps we could replace all these methods with a class called... Serializer.
49+
# See how association is doing the job a serializer again?
50+
# 2. I've seen code like this in many other places.
51+
# Perhaps we should just have it all in one place: Serializer.
52+
# We already have a class called Serializer, I know,
53+
# and that is doing things that are not responsibility of a serializer.
54+
def serializer_class
55+
return @serializer_class if defined?(@serializer_class)
56+
serializer_for_options = { namespace: namespace }
57+
serializer_for_options[:serializer] = reflection_options[:serializer] if reflection_options.key?(:serializer)
58+
@serializer_class = association_options.fetch(:parent_serializer).class.serializer_for(object, serializer_for_options)
59+
end
60+
61+
private
62+
63+
def cached_result
64+
@cached_result ||= {}
65+
end
66+
67+
def serialize_object!(object)
68+
if collection?
69+
if (serializer = instantiate_collection_serializer(object)).nil?
70+
# BUG: per #2027, JSON API resource relationships are only id and type, and hence either
71+
# *require* a serializer or we need to be a little clever about figuring out the id/type.
72+
# In either case, returning the raw virtual value will almost always be incorrect.
73+
#
74+
# Should be reflection_options[:virtual_value] or adapter needs to figure out what to do
75+
# with an object that is non-nil and has no defined serializer.
76+
cached_result[:virtual_value] = object.try(:as_json) || object
77+
else
78+
cached_result[:serializer] = serializer
79+
end
80+
else
81+
cached_result[:serializer] = instantiate_serializer(object)
82+
end
83+
end
84+
85+
# NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection
86+
# serializer. This is a good reason for the reflection to have a to_many? type method.
87+
def instantiate_serializer(object)
88+
serializer_options = association_options.fetch(:parent_serializer_options).except(:serializer)
89+
serializer_options[:serializer_context_class] = association_options.fetch(:parent_serializer).class
90+
serializer = reflection_options.fetch(:serializer, nil)
91+
serializer_options[:serializer] = serializer if serializer
92+
serializer_class.new(object, serializer_options)
93+
end
94+
95+
def instantiate_collection_serializer(object)
96+
serializer = catch(:no_serializer) do
97+
instantiate_serializer(object)
98+
end
99+
serializer
15100
end
16101

17-
def reflection
18-
options[:reflection]
102+
def namespace
103+
reflection_options[:namespace] ||
104+
association_options.fetch(:parent_serializer_options)[:namespace]
19105
end
20106
end
21107
end

lib/active_model/serializer/reflection.rb

Lines changed: 29 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ 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-
5250
def initialize(*)
5351
super
5452
options[:links] = {}
@@ -125,92 +123,6 @@ def collection?
125123
false
126124
end
127125

128-
# Build association. This method is used internally to
129-
# build serializer's association by its reflection.
130-
#
131-
# @param [Serializer] parent_serializer for given association
132-
# @param [Hash{Symbol => Object}] parent_serializer_options
133-
#
134-
# @example
135-
# # Given the following serializer defined:
136-
# class PostSerializer < ActiveModel::Serializer
137-
# has_many :comments, serializer: CommentSummarySerializer
138-
# end
139-
#
140-
# # Then you instantiate your serializer
141-
# post_serializer = PostSerializer.new(post, foo: 'bar') #
142-
# # to build association for comments you need to get reflection
143-
# comments_reflection = PostSerializer._reflections.detect { |r| r.name == :comments }
144-
# # and #build_association
145-
# comments_reflection.build_association(post_serializer, foo: 'bar')
146-
#
147-
# @api private
148-
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
149-
reflection_options = settings.merge(include_data: include_data?(include_slice)) unless block?
150-
151-
association_value = value(parent_serializer, include_slice)
152-
serializer_class = build_serializer_class(association_value, parent_serializer, parent_serializer_options[:namespace])
153-
154-
reflection_options ||= settings.merge(include_data: include_data?(include_slice)) # Needs to be after association_value is evaluated unless reflection.block.nil?
155-
156-
if serializer_class
157-
if (serializer = build_serializer!(association_value, serializer_class, parent_serializer, parent_serializer_options))
158-
reflection_options[:serializer] = serializer
159-
else
160-
# BUG: per #2027, JSON API resource relationships are only id and type, and hence either
161-
# *require* a serializer or we need to be a little clever about figuring out the id/type.
162-
# In either case, returning the raw virtual value will almost always be incorrect.
163-
#
164-
# Should be reflection_options[:virtual_value] or adapter needs to figure out what to do
165-
# with an object that is non-nil and has no defined serializer.
166-
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
167-
end
168-
elsif !association_value.nil? && !association_value.instance_of?(Object)
169-
reflection_options[:virtual_value] = association_value
170-
end
171-
172-
association_block = nil
173-
reflection_options[:reflection] = self
174-
reflection_options[:parent_serializer] = parent_serializer
175-
reflection_options[:parent_serializer_options] = parent_serializer_options
176-
reflection_options[:include_slice] = include_slice
177-
Association.new(name, reflection_options, block)
178-
end
179-
180-
protected
181-
182-
# used in instance exec
183-
attr_accessor :object, :scope
184-
185-
def settings
186-
options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) }
187-
end
188-
189-
# Evaluation of the reflection.block will mutate options.
190-
# So, the settings cannot be used until the block is evaluated.
191-
# This means that each time the block is evaluated, it may set a new
192-
# value in the reflection instance. This is not thread-safe.
193-
# @example
194-
# has_many :likes do
195-
# meta liked: object.likes.any?
196-
# include_data: object.loaded?
197-
# end
198-
def block?
199-
!block.nil?
200-
end
201-
202-
def serializer?
203-
options.key?(:serializer)
204-
end
205-
206-
def serializer
207-
options[:serializer]
208-
end
209-
210-
def namespace
211-
options[:namespace]
212-
end
213-
214126
def include_data?(include_slice)
215127
include_data_setting = options[:include_data_setting]
216128
case include_data_setting
@@ -238,42 +150,39 @@ def value(serializer, include_slice)
238150
end
239151
end
240152

241-
def build_serializer!(association_value, serializer_class, parent_serializer, parent_serializer_options)
242-
if collection?
243-
build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)
244-
else
245-
build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)
246-
end
247-
end
248-
249-
def build_serializer_class(association_value, parent_serializer, parent_serializer_namespace_option)
250-
serializer_for_options = {
251-
# Pass the parent's namespace onto the child serializer
252-
namespace: namespace || parent_serializer_namespace_option
153+
# Build association. This method is used internally to
154+
# build serializer's association by its reflection.
155+
#
156+
# @param [Serializer] parent_serializer for given association
157+
# @param [Hash{Symbol => Object}] parent_serializer_options
158+
#
159+
# @example
160+
# # Given the following serializer defined:
161+
# class PostSerializer < ActiveModel::Serializer
162+
# has_many :comments, serializer: CommentSummarySerializer
163+
# end
164+
#
165+
# # Then you instantiate your serializer
166+
# post_serializer = PostSerializer.new(post, foo: 'bar') #
167+
# # to build association for comments you need to get reflection
168+
# comments_reflection = PostSerializer._reflections.detect { |r| r.name == :comments }
169+
# # and #build_association
170+
# comments_reflection.build_association(post_serializer, foo: 'bar')
171+
#
172+
# @api private
173+
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
174+
association_options = {
175+
parent_serializer: parent_serializer,
176+
parent_serializer_options: parent_serializer_options,
177+
include_slice: include_slice
253178
}
254-
serializer_for_options[:serializer] = serializer if serializer?
255-
parent_serializer.class.serializer_for(association_value, serializer_for_options)
179+
Association.new(self, association_options)
256180
end
257181

258-
# NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection
259-
# serializer.
260-
#
261-
# @return [ActiveModel::Serializer, nil]
262-
def build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)
263-
catch(:no_serializer) do
264-
build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)
265-
end
266-
end
182+
protected
267183

268-
# @return [ActiveModel::Serializer, nil]
269-
def build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)
270-
# Make all the parent serializer instance options available to associations
271-
# except ActiveModelSerializers-specific ones we don't want.
272-
serializer_options = parent_serializer_options.except(:serializer)
273-
serializer_options[:serializer_context_class] = parent_serializer.class
274-
serializer_options[:serializer] = serializer if serializer
275-
serializer_class.new(association_value, serializer_options)
276-
end
184+
# used in instance exec
185+
attr_accessor :object, :scope
277186
end
278187
end
279188
end

lib/active_model_serializers/adapter/json_api/relationship.rb

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,34 @@ def as_json
3434
private
3535

3636
def data_for(association)
37+
if association.collection?
38+
data_for_many(association)
39+
else
40+
data_for_one(association)
41+
end
42+
end
43+
44+
def data_for_one(association)
3745
serializer = association.lazy_association.serializer
38-
if serializer.respond_to?(:each)
39-
serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json }
40-
elsif (virtual_value = association.virtual_value)
46+
if (virtual_value = association.virtual_value)
4147
virtual_value
42-
elsif serializer && serializer.object
48+
elsif serializer && association.object
4349
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
50+
else
51+
nil
52+
end
53+
end
54+
55+
def data_for_many(association)
56+
collection_serializer = association.lazy_association.serializer
57+
if collection_serializer.respond_to?(:each)
58+
collection_serializer.map do |serializer|
59+
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
60+
end
61+
elsif (virtual_value = association.virtual_value)
62+
virtual_value
63+
else
64+
[]
4465
end
4566
end
4667

0 commit comments

Comments
 (0)