rest_framework 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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