rest_framework 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,360 @@
1
+ # This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
2
+ # top-level being either an array or a hash).
3
+ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers::BaseSerializer
4
+ class_attribute :config
5
+ class_attribute :singular_config
6
+ class_attribute :plural_config
7
+ class_attribute :action_config
8
+
9
+ # Accept/ignore `*args` to be compatible with the `ActiveModel::Serializer#initialize` signature.
10
+ def initialize(object=nil, *args, many: nil, model: nil, **kwargs)
11
+ super(object, *args, **kwargs)
12
+
13
+ if many.nil?
14
+ # Determine if we are dealing with many objects or just one.
15
+ @many = @object.is_a?(Enumerable)
16
+ else
17
+ @many = many
18
+ end
19
+
20
+ # Determine model either explicitly, or by inspecting @object or @controller.
21
+ @model = model
22
+ @model ||= @object.class if @object.is_a?(ActiveRecord::Base)
23
+ @model ||= @object[0].class if
24
+ @many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
25
+
26
+ @model ||= @controller.class.get_model if @controller
27
+ end
28
+
29
+ # Get controller action, if possible.
30
+ def get_action
31
+ return @controller&.action_name&.to_sym
32
+ end
33
+
34
+ # Get a locally defined native serializer configuration, if one is defined.
35
+ def get_local_native_serializer_config
36
+ action = self.get_action
37
+
38
+ if action && self.action_config
39
+ # Index action should use :list serializer config if :index is not provided.
40
+ action = :list if action == :index && !self.action_config.key?(:index)
41
+
42
+ return self.action_config[action] if self.action_config[action]
43
+ end
44
+
45
+ # No action_config, so try singular/plural config if explicitly instructed to via @many.
46
+ return self.plural_config if @many == true && self.plural_config
47
+ return self.singular_config if @many == false && self.singular_config
48
+
49
+ # Lastly, try returning the default config, or singular/plural config in that order.
50
+ return self.config || self.singular_config || self.plural_config
51
+ end
52
+
53
+ # Get a native serializer configuration from the controller.
54
+ def get_controller_native_serializer_config
55
+ return nil unless @controller
56
+
57
+ if @many == true
58
+ controller_serializer = @controller.try(:native_serializer_plural_config)
59
+ elsif @many == false
60
+ controller_serializer = @controller.try(:native_serializer_singular_config)
61
+ end
62
+
63
+ return controller_serializer || @controller.try(:native_serializer_config)
64
+ end
65
+
66
+ # Filter a single subconfig for specific keys. By default, keys from `fields` are removed from the
67
+ # provided `subcfg`. There are two (mutually exclusive) options to adjust the behavior:
68
+ #
69
+ # `add`: Add any `fields` to the `subcfg` which aren't already in the `subcfg`.
70
+ # `only`: Remove any values found in the `subcfg` not in `fields`.
71
+ def self.filter_subcfg(subcfg, fields:, add: false, only: false)
72
+ raise "`add` and `only` conflict with one another" if add && only
73
+
74
+ # Don't process nil `subcfg`s.
75
+ return subcfg unless subcfg
76
+
77
+ if subcfg.is_a?(Array)
78
+ subcfg = subcfg.map(&:to_sym)
79
+
80
+ if add
81
+ # Only add fields which are not already included.
82
+ subcfg += fields - subcfg
83
+ elsif only
84
+ subcfg.select! { |c| c.in?(fields) }
85
+ else
86
+ subcfg -= fields
87
+ end
88
+ elsif subcfg.is_a?(Hash)
89
+ subcfg = subcfg.symbolize_keys
90
+
91
+ if add
92
+ # Add doesn't make sense in a hash context since we wouldn't know the values.
93
+ elsif only
94
+ subcfg.select! { |k, _v| k.in?(fields) }
95
+ else
96
+ subcfg.reject! { |k, _v| k.in?(fields) }
97
+ end
98
+ else # Subcfg is a single element (assume string/symbol).
99
+ subcfg = subcfg.to_sym
100
+
101
+ if add
102
+ subcfg = subcfg.in?(fields) ? fields : [subcfg, *fields]
103
+ elsif only
104
+ subcfg = subcfg.in?(fields) ? subcfg : []
105
+ else
106
+ subcfg = subcfg.in?(fields) ? [] : subcfg
107
+ end
108
+ end
109
+
110
+ return subcfg
111
+ end
112
+
113
+ # Filter out configuration properties based on the :except/:only query parameters.
114
+ def filter_from_request(cfg)
115
+ return cfg unless @controller
116
+
117
+ except_param = @controller.try(:native_serializer_except_query_param)
118
+ only_param = @controller.try(:native_serializer_only_query_param)
119
+ if except_param && except = @controller.request&.query_parameters&.[](except_param).presence
120
+ if except = except.split(",").map(&:strip).map(&:to_sym).presence
121
+ # Filter `only`, `except` (additive), `include`, `methods`, and `serializer_methods`.
122
+ if cfg[:only]
123
+ cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: except)
124
+ elsif cfg[:except]
125
+ cfg[:except] = self.class.filter_subcfg(cfg[:except], fields: except, add: true)
126
+ else
127
+ cfg[:except] = except
128
+ end
129
+
130
+ cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: except)
131
+ cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: except)
132
+ cfg[:serializer_methods] = self.class.filter_subcfg(
133
+ cfg[:serializer_methods], fields: except
134
+ )
135
+ cfg[:includes_map] = self.class.filter_subcfg(cfg[:includes_map], fields: except)
136
+ end
137
+ elsif only_param && only = @controller.request&.query_parameters&.[](only_param).presence
138
+ if only = only.split(",").map(&:strip).map(&:to_sym).presence
139
+ # Filter `only`, `include`, and `methods`. Adding anything to `except` is not needed,
140
+ # because any configuration there takes precedence over `only`.
141
+ if cfg[:only]
142
+ cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: only, only: true)
143
+ else
144
+ cfg[:only] = only
145
+ end
146
+
147
+ cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: only, only: true)
148
+ cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: only, only: true)
149
+ cfg[:serializer_methods] = self.class.filter_subcfg(
150
+ cfg[:serializer_methods], fields: only, only: true
151
+ )
152
+ cfg[:includes_map] = self.class.filter_subcfg(cfg[:includes_map], fields: only, only: true)
153
+ end
154
+ end
155
+
156
+ return cfg
157
+ end
158
+
159
+ # Get the associations limit from the controller.
160
+ def _get_associations_limit
161
+ return @_get_associations_limit if defined?(@_get_associations_limit)
162
+
163
+ limit = @controller&.native_serializer_associations_limit
164
+
165
+ # Extract the limit from the query parameters if it's set.
166
+ if query_param = @controller&.native_serializer_associations_limit_query_param
167
+ if @controller.request.query_parameters.key?(query_param)
168
+ query_limit = @controller.request.query_parameters[query_param].to_i
169
+ if query_limit > 0
170
+ limit = query_limit
171
+ else
172
+ limit = nil
173
+ end
174
+ end
175
+ end
176
+
177
+ return @_get_associations_limit = limit
178
+ end
179
+
180
+ # Get a serializer configuration from the controller. `@controller` and `@model` must be set.
181
+ def _get_controller_serializer_config(fields)
182
+ columns = []
183
+ includes = {}
184
+ methods = []
185
+ serializer_methods = {}
186
+
187
+ # We try to construct performant queries using Active Record's `includes` method. This is
188
+ # sometimes impossible, for example when limiting the number of associated records returned, so
189
+ # we should only add associations here when it's useful, and using the `Bullet` gem is helpful
190
+ # in determining when that is the case.
191
+ includes_map = {}
192
+
193
+ column_names = @model.column_names
194
+ reflections = @model.reflections
195
+ attachment_reflections = @model.attachment_reflections
196
+
197
+ fields.each do |f|
198
+ field_config = @controller.class.get_field_config(f)
199
+ next if field_config[:write_only]
200
+
201
+ if f.in?(column_names)
202
+ columns << f
203
+ elsif ref = reflections[f]
204
+ sub_columns = []
205
+ sub_methods = []
206
+ field_config[:sub_fields].each do |sf|
207
+ if !ref.polymorphic? && sf.in?(ref.klass.column_names)
208
+ sub_columns << sf
209
+ else
210
+ sub_methods << sf
211
+ end
212
+ end
213
+ sub_config = {only: sub_columns, methods: sub_methods}
214
+
215
+ # Apply certain rules regarding collection associations.
216
+ if ref.collection?
217
+ # If we need to limit the number of serialized association records, then dynamically add a
218
+ # serializer method to do so.
219
+ if limit = self._get_associations_limit
220
+ serializer_methods[f] = f
221
+ self.define_singleton_method(f) do |record|
222
+ next record.send(f).limit(limit).as_json(**sub_config)
223
+ end
224
+ else
225
+ includes[f] = sub_config
226
+ includes_map[f] = f.to_sym
227
+ end
228
+
229
+ # If we need to include the association count, then add it here.
230
+ if @controller.native_serializer_include_associations_count
231
+ method_name = "#{f}.count"
232
+ serializer_methods[method_name] = method_name
233
+ self.define_singleton_method(method_name) do |record|
234
+ next record.send(f).count
235
+ end
236
+ end
237
+ else
238
+ includes[f] = sub_config
239
+ includes_map[f] = f.to_sym
240
+ end
241
+ elsif ref = reflections["rich_text_#{f}"]
242
+ # ActionText Integration: Define rich text serializer method.
243
+ includes_map[f] = :"rich_text_#{f}"
244
+ serializer_methods[f] = f
245
+ self.define_singleton_method(f) do |record|
246
+ next record.send(f).to_s
247
+ end
248
+ elsif ref = attachment_reflections[f]
249
+ # ActiveStorage Integration: Define attachment serializer method.
250
+ if ref.macro == :has_one_attached
251
+ serializer_methods[f] = f
252
+ includes_map[f] = {"#{f}_attachment": :blob}
253
+ self.define_singleton_method(f) do |record|
254
+ next record.send(f).attachment&.url
255
+ end
256
+ elsif ref.macro == :has_many_attached
257
+ serializer_methods[f] = f
258
+ includes_map[f] = {"#{f}_attachments": :blob}
259
+ self.define_singleton_method(f) do |record|
260
+ # Iterating the collection yields attachment objects.
261
+ next record.send(f).map(&:url)
262
+ end
263
+ end
264
+ elsif @model.method_defined?(f)
265
+ methods << f
266
+ end
267
+ end
268
+
269
+ return {
270
+ only: columns,
271
+ include: includes,
272
+ methods: methods,
273
+ serializer_methods: serializer_methods,
274
+ includes_map: includes_map,
275
+ }
276
+ end
277
+
278
+ # Get the raw serializer config, prior to any adjustments from the request.
279
+ #
280
+ # Use `deep_dup` on any class mutables (array, hash, etc) to avoid mutating class state.
281
+ def get_raw_serializer_config
282
+ # Return a locally defined serializer config if one is defined.
283
+ if local_config = self.get_local_native_serializer_config
284
+ return local_config.deep_dup
285
+ end
286
+
287
+ # Return a serializer config if one is defined on the controller.
288
+ if serializer_config = self.get_controller_native_serializer_config
289
+ return serializer_config.deep_dup
290
+ end
291
+
292
+ # If the config wasn't determined, build a serializer config from controller fields.
293
+ if @model && fields = @controller&.get_fields
294
+ return self._get_controller_serializer_config(fields.deep_dup)
295
+ end
296
+
297
+ # By default, pass an empty configuration, using the default Rails serializer.
298
+ return {}
299
+ end
300
+
301
+ # Get a configuration passable to `serializable_hash` for the object, filtered if required.
302
+ def get_serializer_config
303
+ return self.filter_from_request(self.get_raw_serializer_config)
304
+ end
305
+
306
+ # Serialize a single record and merge results of `serializer_methods`.
307
+ def _serialize(record, config, serializer_methods)
308
+ # Ensure serializer_methods is either falsy, or a hash.
309
+ if serializer_methods && !serializer_methods.is_a?(Hash)
310
+ serializer_methods = [serializer_methods].flatten.map { |m| [m, m] }.to_h
311
+ end
312
+
313
+ # Merge serialized record with any serializer method results.
314
+ return record.serializable_hash(config).merge(
315
+ serializer_methods&.map { |m, k| [k.to_sym, self.send(m, record)] }.to_h,
316
+ )
317
+ end
318
+
319
+ def serialize(*args)
320
+ config = self.get_serializer_config
321
+ serializer_methods = config.delete(:serializer_methods)
322
+ includes_map = config.delete(:includes_map)
323
+
324
+ if @object.respond_to?(:to_ary)
325
+ # Preload associations using `includes` to avoid N+1 queries. For now this also allows filter
326
+ # backends to use associated data; perhaps it may be wise to have a system in place for
327
+ # filters to preload their own associations?
328
+ @object = @object.includes(*includes_map.values) if includes_map.present?
329
+
330
+ return @object.map { |r| self._serialize(r, config, serializer_methods) }
331
+ end
332
+
333
+ return self._serialize(@object, config, serializer_methods)
334
+ end
335
+
336
+ # Allow a serializer instance to be used as a hash directly in a nested serializer config.
337
+ def [](key)
338
+ @_nested_config ||= self.get_serializer_config
339
+ return @_nested_config[key]
340
+ end
341
+
342
+ def []=(key, value)
343
+ @_nested_config ||= self.get_serializer_config
344
+ return @_nested_config[key] = value
345
+ end
346
+
347
+ # Allow a serializer class to be used as a hash directly in a nested serializer config.
348
+ def self.[](key)
349
+ @_nested_config ||= self.new.get_serializer_config
350
+ return @_nested_config[key]
351
+ end
352
+
353
+ def self.[]=(key, value)
354
+ @_nested_config ||= self.new.get_serializer_config
355
+ return @_nested_config[key] = value
356
+ end
357
+ end
358
+
359
+ # Alias for convenience.
360
+ RESTFramework::NativeSerializer = RESTFramework::Serializers::NativeSerializer