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.
- checksums.yaml +4 -4
- data/.yardopts +5 -0
- data/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +1 -1
- data/lib/rest_framework/filters/{base.rb → base_filter.rb} +4 -1
- data/lib/rest_framework/filters/{model_ordering.rb → model_ordering_filter.rb} +4 -1
- data/lib/rest_framework/filters/{model_query.rb → model_query_filter.rb} +4 -1
- data/lib/rest_framework/filters/{model_search.rb → model_search_filter.rb} +4 -1
- data/lib/rest_framework/filters/{ransack.rb → ransack_filter.rb} +4 -1
- data/lib/rest_framework/filters.rb +5 -5
- data/lib/rest_framework/{controller_mixins/base.rb → mixins/base_controller_mixin.rb} +4 -5
- data/lib/rest_framework/{controller_mixins/bulk.rb → mixins/bulk_model_controller_mixin.rb} +17 -5
- data/lib/rest_framework/{controller_mixins/models.rb → mixins/model_controller_mixin.rb} +39 -44
- data/lib/rest_framework/mixins.rb +7 -0
- data/lib/rest_framework/paginators/base_paginator.rb +19 -0
- data/lib/rest_framework/paginators/page_number_paginator.rb +73 -0
- data/lib/rest_framework/paginators.rb +3 -84
- data/lib/rest_framework/routers.rb +0 -1
- data/lib/rest_framework/serializers/active_model_serializer_adapter_factory.rb +20 -0
- data/lib/rest_framework/serializers/base_serializer.rb +40 -0
- data/lib/rest_framework/serializers/native_serializer.rb +360 -0
- data/lib/rest_framework/serializers.rb +4 -383
- data/lib/rest_framework.rb +1 -1
- metadata +17 -12
- data/lib/rest_framework/controller_mixins.rb +0 -7
@@ -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
|