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.
- 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
|