rest_framework 1.0.0.rc1 → 1.0.1
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/README.md +6 -2
- data/VERSION +1 -1
- data/app/views/rest_framework/_head.html.erb +3 -1
- data/app/views/rest_framework/_heading.html.erb +1 -0
- data/app/views/rest_framework/head/_extra.html.erb +0 -0
- data/app/views/rest_framework/heading/_actions.html.erb +2 -1
- data/app/views/rest_framework/heading/_extra.html.erb +0 -0
- data/app/views/rest_framework/heading/actions/_extra.html.erb +0 -0
- data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +2 -2
- data/lib/rest_framework/errors/nil_passed_to_render_api_error.rb +1 -1
- data/lib/rest_framework/errors/unknown_model_error.rb +1 -1
- data/lib/rest_framework/filters/ordering_filter.rb +3 -3
- data/lib/rest_framework/filters/query_filter.rb +14 -14
- data/lib/rest_framework/filters/ransack_filter.rb +1 -1
- data/lib/rest_framework/filters/search_filter.rb +3 -3
- data/lib/rest_framework/generators/controller_generator.rb +2 -2
- data/lib/rest_framework/mixins/base_controller_mixin.rb +34 -33
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +8 -15
- data/lib/rest_framework/mixins/model_controller_mixin.rb +91 -65
- data/lib/rest_framework/paginators/page_number_paginator.rb +6 -6
- data/lib/rest_framework/routers.rb +10 -16
- data/lib/rest_framework/serializers/active_model_serializer_adapter_factory.rb +4 -2
- data/lib/rest_framework/serializers/base_serializer.rb +7 -5
- data/lib/rest_framework/serializers/native_serializer.rb +82 -129
- data/lib/rest_framework/utils.rb +27 -26
- data/lib/rest_framework/version.rb +1 -1
- data/lib/rest_framework.rb +24 -17
- data/vendor/assets/javascripts/rest_framework/external.min.js +4 -4
- data/vendor/assets/stylesheets/rest_framework/external.min.css +3 -3
- metadata +8 -5
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
# This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
|
|
2
2
|
# top-level being either an array or a hash).
|
|
3
3
|
class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers::BaseSerializer
|
|
4
|
+
EXTRACT_FROM_QUERY = ->(p, controller) {
|
|
5
|
+
return Set[] if p.blank?
|
|
6
|
+
(
|
|
7
|
+
controller.request&.query_parameters&.[](p).presence&.split(",")&.map { |x|
|
|
8
|
+
x.strip.presence
|
|
9
|
+
}&.compact || []
|
|
10
|
+
).to_set
|
|
11
|
+
}
|
|
4
12
|
class_attribute :config
|
|
5
13
|
class_attribute :singular_config
|
|
6
14
|
class_attribute :plural_config
|
|
7
15
|
class_attribute :action_config
|
|
8
16
|
|
|
9
17
|
# Accept/ignore `*args` to be compatible with the `ActiveModel::Serializer#initialize` signature.
|
|
10
|
-
def initialize(object=nil, *args, many: nil, model: nil, **kwargs)
|
|
18
|
+
def initialize(object = nil, *args, many: nil, model: nil, **kwargs)
|
|
11
19
|
super(object, *args, **kwargs)
|
|
12
20
|
|
|
13
21
|
if many.nil?
|
|
@@ -26,28 +34,66 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
26
34
|
@model ||= @controller.class.get_model if @controller
|
|
27
35
|
end
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return @controller&.action_name&.to_sym
|
|
37
|
+
def action
|
|
38
|
+
@action ||= @controller&.action_name&.to_sym
|
|
32
39
|
end
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
def fields
|
|
42
|
+
return @fields if defined?(@fields)
|
|
43
|
+
return nil unless base_fields = @controller&.get_fields
|
|
44
|
+
|
|
45
|
+
only_param = @controller.class.native_serializer_only_query_param
|
|
46
|
+
except_param = @controller.class.native_serializer_except_query_param
|
|
47
|
+
include_param = @controller.class.native_serializer_include_query_param
|
|
48
|
+
exclude_param = @controller.class.native_serializer_exclude_query_param
|
|
49
|
+
|
|
50
|
+
only = EXTRACT_FROM_QUERY.call(only_param, @controller)
|
|
51
|
+
except = EXTRACT_FROM_QUERY.call(except_param, @controller)
|
|
52
|
+
include = EXTRACT_FROM_QUERY.call(include_param, @controller)
|
|
53
|
+
exclude = EXTRACT_FROM_QUERY.call(exclude_param, @controller)
|
|
54
|
+
|
|
55
|
+
field_configuration = @controller.class.field_configuration
|
|
56
|
+
@fields = base_fields.select do |f|
|
|
57
|
+
cfg = field_configuration[f]
|
|
58
|
+
|
|
59
|
+
# We never serialize write-only fields.
|
|
60
|
+
next false if cfg[:write_only]
|
|
61
|
+
|
|
62
|
+
# We never serialize `hidden_from_index` fields for collections as this is a performance
|
|
63
|
+
# option.
|
|
64
|
+
next false if cfg[:hidden_from_index] && @many
|
|
65
|
+
|
|
66
|
+
# Explicitly excluded fields should never be serialized.
|
|
67
|
+
next false if f.in?(except) || f.in?(exclude)
|
|
68
|
+
|
|
69
|
+
# Hidden fields must be in `only` or `include` to be serialized; for non-hidden fields, either
|
|
70
|
+
# `only` must be empty, or the field must be in `only` or `include`.
|
|
71
|
+
if cfg[:hidden]
|
|
72
|
+
next true if f.in?(only) || f.in?(include)
|
|
73
|
+
elsif only.empty? || f.in?(only) || f.in?(include)
|
|
74
|
+
next true
|
|
75
|
+
end
|
|
37
76
|
|
|
38
|
-
|
|
77
|
+
next false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@fields
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def get_local_native_serializer_config
|
|
84
|
+
if (action = self.action) && (cfg = action_config)
|
|
39
85
|
# Index action should use :list serializer config if :index is not provided.
|
|
40
|
-
action = :list if action == :index && !
|
|
86
|
+
action = :list if action == :index && !cfg.key?(:index)
|
|
41
87
|
|
|
42
|
-
return
|
|
88
|
+
return cfg[action] if cfg[action]
|
|
43
89
|
end
|
|
44
90
|
|
|
45
91
|
# No action_config, so try singular/plural config if explicitly instructed to via @many.
|
|
46
92
|
return self.plural_config if @many == true && self.plural_config
|
|
47
93
|
return self.singular_config if @many == false && self.singular_config
|
|
48
94
|
|
|
49
|
-
# Lastly, try returning the default config
|
|
50
|
-
|
|
95
|
+
# Lastly, try returning the default config.
|
|
96
|
+
self.config
|
|
51
97
|
end
|
|
52
98
|
|
|
53
99
|
# Get a native serializer configuration from the controller.
|
|
@@ -60,105 +106,12 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
60
106
|
controller_serializer = @controller.class.native_serializer_singular_config
|
|
61
107
|
end
|
|
62
108
|
|
|
63
|
-
|
|
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.class.native_serializer_except_query_param
|
|
118
|
-
only_param = @controller.class.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
|
|
109
|
+
controller_serializer || @controller.class.native_serializer_config
|
|
157
110
|
end
|
|
158
111
|
|
|
159
112
|
# Get the associations limit from the controller.
|
|
160
|
-
def
|
|
161
|
-
return @
|
|
113
|
+
def _associations_limit
|
|
114
|
+
return @_associations_limit if defined?(@_associations_limit)
|
|
162
115
|
|
|
163
116
|
limit = @controller&.class&.native_serializer_associations_limit
|
|
164
117
|
|
|
@@ -174,11 +127,11 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
174
127
|
end
|
|
175
128
|
end
|
|
176
129
|
|
|
177
|
-
|
|
130
|
+
@_associations_limit = limit
|
|
178
131
|
end
|
|
179
132
|
|
|
180
133
|
# Get a serializer configuration from the controller. `@controller` and `@model` must be set.
|
|
181
|
-
def _get_controller_serializer_config
|
|
134
|
+
def _get_controller_serializer_config
|
|
182
135
|
columns = []
|
|
183
136
|
includes = {}
|
|
184
137
|
methods = []
|
|
@@ -194,7 +147,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
194
147
|
reflections = @model.reflections
|
|
195
148
|
attachment_reflections = @model.attachment_reflections
|
|
196
149
|
|
|
197
|
-
fields.each do |f|
|
|
150
|
+
self.fields.each do |f|
|
|
198
151
|
field_config = @controller.class.field_configuration[f]
|
|
199
152
|
next if field_config[:write_only]
|
|
200
153
|
|
|
@@ -210,13 +163,13 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
210
163
|
sub_methods << sf
|
|
211
164
|
end
|
|
212
165
|
end
|
|
213
|
-
sub_config = {only: sub_columns, methods: sub_methods}
|
|
166
|
+
sub_config = { only: sub_columns, methods: sub_methods }
|
|
214
167
|
|
|
215
168
|
# Apply certain rules regarding collection associations.
|
|
216
169
|
if ref.collection?
|
|
217
170
|
# If we need to limit the number of serialized association records, then dynamically add a
|
|
218
171
|
# serializer method to do so.
|
|
219
|
-
if limit = self.
|
|
172
|
+
if limit = self._associations_limit
|
|
220
173
|
serializer_methods[f] = f
|
|
221
174
|
self.define_singleton_method(f) do |record|
|
|
222
175
|
next record.send(f).limit(limit).as_json(**sub_config)
|
|
@@ -257,7 +210,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
257
210
|
# ActiveStorage Integration: Define attachment serializer method.
|
|
258
211
|
if ref.macro == :has_one_attached
|
|
259
212
|
serializer_methods[f] = f
|
|
260
|
-
includes_map[f] = {"#{f}_attachment": :blob}
|
|
213
|
+
includes_map[f] = { "#{f}_attachment": :blob }
|
|
261
214
|
self.define_singleton_method(f) do |record|
|
|
262
215
|
attached = record.send(f)
|
|
263
216
|
next attached.attachment ? {
|
|
@@ -268,7 +221,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
268
221
|
end
|
|
269
222
|
elsif ref.macro == :has_many_attached
|
|
270
223
|
serializer_methods[f] = f
|
|
271
|
-
includes_map[f] = {"#{f}_attachments": :blob}
|
|
224
|
+
includes_map[f] = { "#{f}_attachments": :blob }
|
|
272
225
|
self.define_singleton_method(f) do |record|
|
|
273
226
|
# Iterating the collection yields attachment objects.
|
|
274
227
|
next record.send(f).map { |a|
|
|
@@ -288,7 +241,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
288
241
|
end
|
|
289
242
|
end
|
|
290
243
|
|
|
291
|
-
|
|
244
|
+
{
|
|
292
245
|
only: columns,
|
|
293
246
|
include: includes,
|
|
294
247
|
methods: methods,
|
|
@@ -312,29 +265,29 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
312
265
|
end
|
|
313
266
|
|
|
314
267
|
# If the config wasn't determined, build a serializer config from controller fields.
|
|
315
|
-
if @model && fields
|
|
316
|
-
return self._get_controller_serializer_config
|
|
268
|
+
if @model && self.fields
|
|
269
|
+
return self._get_controller_serializer_config
|
|
317
270
|
end
|
|
318
271
|
|
|
319
272
|
# By default, pass an empty configuration, using the default Rails serializer.
|
|
320
|
-
|
|
273
|
+
{}
|
|
321
274
|
end
|
|
322
275
|
|
|
323
|
-
# Get a configuration passable to `serializable_hash` for the object
|
|
276
|
+
# Get a configuration passable to `serializable_hash` for the object.
|
|
324
277
|
def get_serializer_config
|
|
325
|
-
|
|
278
|
+
self.get_raw_serializer_config
|
|
326
279
|
end
|
|
327
280
|
|
|
328
281
|
# Serialize a single record and merge results of `serializer_methods`.
|
|
329
282
|
def _serialize(record, config, serializer_methods)
|
|
330
283
|
# Ensure serializer_methods is either falsy, or a hash.
|
|
331
284
|
if serializer_methods && !serializer_methods.is_a?(Hash)
|
|
332
|
-
serializer_methods = [serializer_methods].flatten.map { |m| [m, m] }.to_h
|
|
285
|
+
serializer_methods = [ serializer_methods ].flatten.map { |m| [ m, m ] }.to_h
|
|
333
286
|
end
|
|
334
287
|
|
|
335
288
|
# Merge serialized record with any serializer method results.
|
|
336
|
-
|
|
337
|
-
serializer_methods&.map { |m, k| [k.to_sym, self.send(m, record)] }.to_h,
|
|
289
|
+
record.serializable_hash(config).merge(
|
|
290
|
+
serializer_methods&.map { |m, k| [ k.to_sym, self.send(m, record) ] }.to_h,
|
|
338
291
|
)
|
|
339
292
|
end
|
|
340
293
|
|
|
@@ -352,29 +305,29 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
352
305
|
return @object.map { |r| self._serialize(r, config, serializer_methods) }
|
|
353
306
|
end
|
|
354
307
|
|
|
355
|
-
|
|
308
|
+
self._serialize(@object, config, serializer_methods)
|
|
356
309
|
end
|
|
357
310
|
|
|
358
311
|
# Allow a serializer instance to be used as a hash directly in a nested serializer config.
|
|
359
312
|
def [](key)
|
|
360
313
|
@_nested_config ||= self.get_serializer_config
|
|
361
|
-
|
|
314
|
+
@_nested_config[key]
|
|
362
315
|
end
|
|
363
316
|
|
|
364
317
|
def []=(key, value)
|
|
365
318
|
@_nested_config ||= self.get_serializer_config
|
|
366
|
-
|
|
319
|
+
@_nested_config[key] = value
|
|
367
320
|
end
|
|
368
321
|
|
|
369
322
|
# Allow a serializer class to be used as a hash directly in a nested serializer config.
|
|
370
323
|
def self.[](key)
|
|
371
324
|
@_nested_config ||= self.new.get_serializer_config
|
|
372
|
-
|
|
325
|
+
@_nested_config[key]
|
|
373
326
|
end
|
|
374
327
|
|
|
375
328
|
def self.[]=(key, value)
|
|
376
329
|
@_nested_config ||= self.new.get_serializer_config
|
|
377
|
-
|
|
330
|
+
@_nested_config[key] = value
|
|
378
331
|
end
|
|
379
332
|
end
|
|
380
333
|
|
data/lib/rest_framework/utils.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module RESTFramework::Utils
|
|
2
|
-
HTTP_VERB_ORDERING = %w
|
|
2
|
+
HTTP_VERB_ORDERING = %w[GET POST PUT PATCH DELETE OPTIONS HEAD]
|
|
3
3
|
|
|
4
4
|
# Convert `extra_actions` hash to a consistent format: `{path:, methods:, metadata:, kwargs:}`.
|
|
5
5
|
def self.parse_extra_actions(extra_actions)
|
|
6
|
-
|
|
6
|
+
(extra_actions || {}).map { |k, v|
|
|
7
7
|
path = k
|
|
8
8
|
kwargs = {}
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@ module RESTFramework::Utils
|
|
|
13
13
|
v = v.symbolize_keys
|
|
14
14
|
|
|
15
15
|
# Cast method/methods to an array.
|
|
16
|
-
methods = [v.delete(:methods), v.delete(:method)].flatten.compact
|
|
16
|
+
methods = [ v.delete(:methods), v.delete(:method) ].flatten.compact
|
|
17
17
|
|
|
18
18
|
# Override path if it's provided.
|
|
19
19
|
if v.key?(:path)
|
|
@@ -26,7 +26,7 @@ module RESTFramework::Utils
|
|
|
26
26
|
# Pass any further kwargs to the underlying Rails interface.
|
|
27
27
|
kwargs = v
|
|
28
28
|
else
|
|
29
|
-
methods = [v].flatten
|
|
29
|
+
methods = [ v ].flatten
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
next [
|
|
@@ -41,10 +41,11 @@ module RESTFramework::Utils
|
|
|
41
41
|
}.to_h
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
def self.get_skipped_builtin_actions(controller_class, singular)
|
|
45
|
+
(
|
|
46
|
+
(
|
|
47
|
+
RESTFramework::BUILTIN_ACTIONS.keys - (singular ? [ :index ] : [])
|
|
48
|
+
) + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
|
|
48
49
|
).reject do |action|
|
|
49
50
|
controller_class.method_defined?(action)
|
|
50
51
|
end
|
|
@@ -58,7 +59,7 @@ module RESTFramework::Utils
|
|
|
58
59
|
# Normalize a path pattern by replacing URL params with generic placeholder, and removing the
|
|
59
60
|
# `(.:format)` at the end.
|
|
60
61
|
def self.comparable_path(path)
|
|
61
|
-
|
|
62
|
+
path.gsub("(.:format)", "").gsub(/:[0-9A-Za-z_-]+/, ":x")
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
# Show routes under a controller action; used for the browsable API.
|
|
@@ -74,7 +75,7 @@ module RESTFramework::Utils
|
|
|
74
75
|
|
|
75
76
|
# Return routes that match our current route subdomain/pattern, grouped by controller. We
|
|
76
77
|
# precompute certain properties of the route for performance.
|
|
77
|
-
|
|
78
|
+
application_routes.routes.select { |r|
|
|
78
79
|
# We `select` first to avoid unnecessarily calculating metadata for routes we don't even want
|
|
79
80
|
# to show.
|
|
80
81
|
(r.defaults[:subdomain].blank? || r.defaults[:subdomain] == request.subdomain) &&
|
|
@@ -100,7 +101,7 @@ module RESTFramework::Utils
|
|
|
100
101
|
verb: r.verb,
|
|
101
102
|
path: path,
|
|
102
103
|
path_with_params: r.format(
|
|
103
|
-
r.required_parts.each_with_index.map { |p, i| [p, path_params[i]] }.to_h,
|
|
104
|
+
r.required_parts.each_with_index.map { |p, i| [ p, path_params[i] ] }.to_h,
|
|
104
105
|
),
|
|
105
106
|
relative_path: relative_path,
|
|
106
107
|
concat_path: concat_path,
|
|
@@ -125,17 +126,17 @@ module RESTFramework::Utils
|
|
|
125
126
|
# Sort the controller groups by current controller first, then alphanumerically.
|
|
126
127
|
# Note: Use `controller_path` instead of `params[:controller]` to avoid re-raising a
|
|
127
128
|
# `ActionDispatch::Http::Parameters::ParseError` exception.
|
|
128
|
-
[request.controller_class.controller_path == c ? 0 : 1, c]
|
|
129
|
+
[ request.controller_class.controller_path == c ? 0 : 1, c ]
|
|
129
130
|
}.to_h
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
# Custom inflector for RESTful controllers.
|
|
133
|
-
def self.inflect(s, acronyms=nil)
|
|
134
|
+
def self.inflect(s, acronyms = nil)
|
|
134
135
|
acronyms&.each do |acronym|
|
|
135
136
|
s = s.gsub(/\b#{acronym}\b/i, acronym)
|
|
136
137
|
end
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
s
|
|
139
140
|
end
|
|
140
141
|
|
|
141
142
|
# Parse fields hashes.
|
|
@@ -153,12 +154,12 @@ module RESTFramework::Utils
|
|
|
153
154
|
parsed_fields -= h[:except].map(&:to_s) if h[:except]
|
|
154
155
|
|
|
155
156
|
# Warn for any unknown keys.
|
|
156
|
-
(h.keys - [:only, :except, :include, :exclude]).each do |k|
|
|
157
|
-
Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
|
|
157
|
+
(h.keys - [ :only, :except, :include, :exclude ]).each do |k|
|
|
158
|
+
Rails.logger.warn("RRF: Unknown key in fields hash: #{k}.")
|
|
158
159
|
end
|
|
159
160
|
|
|
160
161
|
# We should always return strings, not symbols.
|
|
161
|
-
|
|
162
|
+
parsed_fields.map(&:to_s)
|
|
162
163
|
end
|
|
163
164
|
|
|
164
165
|
# Get the fields for a given model, including not just columns (which includes
|
|
@@ -181,7 +182,7 @@ module RESTFramework::Utils
|
|
|
181
182
|
# Associations:
|
|
182
183
|
associations = model.reflections.map { |association, ref|
|
|
183
184
|
# Ignore associations for which we have custom integrations.
|
|
184
|
-
if ref.class_name.in?(%w
|
|
185
|
+
if ref.class_name.in?(%w[ActionText::RichText ActiveStorage::Attachment ActiveStorage::Blob])
|
|
185
186
|
next nil
|
|
186
187
|
end
|
|
187
188
|
|
|
@@ -194,29 +195,29 @@ module RESTFramework::Utils
|
|
|
194
195
|
next association
|
|
195
196
|
}.compact
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
base_fields + associations + atf + asf
|
|
198
199
|
end
|
|
199
200
|
|
|
200
201
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
|
201
202
|
def self.sub_fields_for(ref)
|
|
202
203
|
if !ref.polymorphic? && model = ref.klass
|
|
203
|
-
sub_fields = [model.primary_key].flatten.compact
|
|
204
|
+
sub_fields = [ model.primary_key ].flatten.compact
|
|
204
205
|
label_fields = RESTFramework.config.label_fields
|
|
205
206
|
|
|
206
|
-
#
|
|
207
|
+
# Preferably find a database column to use as label.
|
|
207
208
|
if match = label_fields.find { |f| f.in?(model.column_names) }
|
|
208
|
-
return sub_fields + [match]
|
|
209
|
+
return sub_fields + [ match ]
|
|
209
210
|
end
|
|
210
211
|
|
|
211
212
|
# Otherwise, find a method.
|
|
212
213
|
if match = label_fields.find { |f| model.method_defined?(f) }
|
|
213
|
-
return sub_fields + [match]
|
|
214
|
+
return sub_fields + [ match ]
|
|
214
215
|
end
|
|
215
216
|
|
|
216
217
|
return sub_fields
|
|
217
218
|
end
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
[ "id", "name" ]
|
|
220
221
|
end
|
|
221
222
|
|
|
222
223
|
# Get a field's id/ids variation.
|
|
@@ -229,7 +230,7 @@ module RESTFramework::Utils
|
|
|
229
230
|
return reflection.foreign_key
|
|
230
231
|
end
|
|
231
232
|
|
|
232
|
-
|
|
233
|
+
nil
|
|
233
234
|
end
|
|
234
235
|
|
|
235
236
|
# Wrap a serializer with an adapter if it is an ActiveModel::Serializer.
|
|
@@ -238,6 +239,6 @@ module RESTFramework::Utils
|
|
|
238
239
|
return RESTFramework::ActiveModelSerializerAdapterFactory.for(s)
|
|
239
240
|
end
|
|
240
241
|
|
|
241
|
-
|
|
242
|
+
s
|
|
242
243
|
end
|
|
243
244
|
end
|
data/lib/rest_framework.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RESTFramework
|
|
4
|
+
BUILTIN_FORM_ACTIONS = %i[new edit].freeze
|
|
4
5
|
BUILTIN_ACTIONS = {
|
|
5
6
|
index: :get,
|
|
6
7
|
new: :get,
|
|
@@ -9,14 +10,14 @@ module RESTFramework
|
|
|
9
10
|
BUILTIN_MEMBER_ACTIONS = {
|
|
10
11
|
show: :get,
|
|
11
12
|
edit: :get,
|
|
12
|
-
update: [:put, :patch].freeze,
|
|
13
|
+
update: [ :put, :patch ].freeze,
|
|
13
14
|
destroy: :delete,
|
|
14
15
|
}.freeze
|
|
15
16
|
RRF_BUILTIN_ACTIONS = {
|
|
16
17
|
options: :options,
|
|
17
18
|
}.freeze
|
|
18
19
|
RRF_BUILTIN_BULK_ACTIONS = {
|
|
19
|
-
update_all: [:put, :patch].freeze,
|
|
20
|
+
update_all: [ :put, :patch ].freeze,
|
|
20
21
|
destroy_all: :delete,
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
@@ -35,12 +36,12 @@ module RESTFramework
|
|
|
35
36
|
EXTERNAL_ASSETS = {
|
|
36
37
|
# Bootstrap
|
|
37
38
|
"bootstrap.min.css" => {
|
|
38
|
-
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.
|
|
39
|
-
sri: "sha384-
|
|
39
|
+
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css",
|
|
40
|
+
sri: "sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr",
|
|
40
41
|
},
|
|
41
42
|
"bootstrap.min.js" => {
|
|
42
|
-
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.
|
|
43
|
-
sri: "sha384-
|
|
43
|
+
url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js",
|
|
44
|
+
sri: "sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q",
|
|
44
45
|
},
|
|
45
46
|
|
|
46
47
|
# Bootstrap Icons
|
|
@@ -65,12 +66,12 @@ module RESTFramework
|
|
|
65
66
|
"highlight-a11y-dark.min.css" => {
|
|
66
67
|
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/a11y-dark.min.css",
|
|
67
68
|
sri: "sha512-Vj6gPCk8EZlqnoveEyuGyYaWZ1+jyjMPg8g4shwyyNlRQl6d3L9At02ZHQr5K6s5duZl/+YKMnM3/8pDhoUphg==",
|
|
68
|
-
extra_tag_attrs: {class: "rrf-dark-mode"},
|
|
69
|
+
extra_tag_attrs: { class: "rrf-dark-mode" },
|
|
69
70
|
},
|
|
70
71
|
"highlight-a11y-light.min.css" => {
|
|
71
72
|
url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/a11y-light.min.css",
|
|
72
73
|
sri: "sha512-WDk6RzwygsN9KecRHAfm9HTN87LQjqdygDmkHSJxVkVI7ErCZ8ZWxP6T8RvBujY1n2/E4Ac+bn2ChXnp5rnnHA==",
|
|
73
|
-
extra_tag_attrs: {class: "rrf-light-mode"},
|
|
74
|
+
extra_tag_attrs: { class: "rrf-light-mode" },
|
|
74
75
|
},
|
|
75
76
|
|
|
76
77
|
# NeatJSON
|
|
@@ -117,7 +118,7 @@ module RESTFramework
|
|
|
117
118
|
raise "Unknown asset extension: #{ext}."
|
|
118
119
|
end
|
|
119
120
|
|
|
120
|
-
[name, cfg]
|
|
121
|
+
[ name, cfg ]
|
|
121
122
|
}.to_h.freeze
|
|
122
123
|
# rubocop:enable Layout/LineLength
|
|
123
124
|
|
|
@@ -126,9 +127,9 @@ module RESTFramework
|
|
|
126
127
|
# Global configuration should be kept minimal, as controller-level configurations allows multiple
|
|
127
128
|
# APIs to be defined to behave differently.
|
|
128
129
|
class Config
|
|
129
|
-
DEFAULT_LABEL_FIELDS = %w
|
|
130
|
-
DEFAULT_SEARCH_COLUMNS = DEFAULT_LABEL_FIELDS + %w
|
|
131
|
-
|
|
130
|
+
DEFAULT_LABEL_FIELDS = %w[name label login title email username url].freeze
|
|
131
|
+
DEFAULT_SEARCH_COLUMNS = DEFAULT_LABEL_FIELDS + %w[description note].freeze
|
|
132
|
+
DEFAULT_READ_ONLY_FIELDS = %w[
|
|
132
133
|
created_at
|
|
133
134
|
created_by
|
|
134
135
|
created_by_id
|
|
@@ -139,6 +140,10 @@ module RESTFramework
|
|
|
139
140
|
utf8
|
|
140
141
|
authenticity_token
|
|
141
142
|
].freeze
|
|
143
|
+
DEFAULT_WRITE_ONLY_FIELDS = %w[
|
|
144
|
+
password
|
|
145
|
+
password_confirmation
|
|
146
|
+
].freeze
|
|
142
147
|
|
|
143
148
|
# Permits use of `render(api: obj)` syntax over `render_api(obj)`; `true` by default.
|
|
144
149
|
attr_accessor :register_api_renderer
|
|
@@ -172,8 +177,9 @@ module RESTFramework
|
|
|
172
177
|
# The default search columns to use when generating search filters.
|
|
173
178
|
attr_accessor :search_columns
|
|
174
179
|
|
|
175
|
-
#
|
|
176
|
-
attr_accessor :
|
|
180
|
+
# Helper to set global read/write only fields.
|
|
181
|
+
attr_accessor :read_only_fields
|
|
182
|
+
attr_accessor :write_only_fields
|
|
177
183
|
|
|
178
184
|
# Option to use vendored assets (requires sprockets or propshaft) rather than linking to
|
|
179
185
|
# external assets (the default).
|
|
@@ -187,12 +193,13 @@ module RESTFramework
|
|
|
187
193
|
|
|
188
194
|
self.label_fields = DEFAULT_LABEL_FIELDS
|
|
189
195
|
self.search_columns = DEFAULT_SEARCH_COLUMNS
|
|
190
|
-
self.
|
|
196
|
+
self.read_only_fields = DEFAULT_READ_ONLY_FIELDS
|
|
197
|
+
self.write_only_fields = DEFAULT_WRITE_ONLY_FIELDS
|
|
191
198
|
end
|
|
192
199
|
end
|
|
193
200
|
|
|
194
201
|
def self.config
|
|
195
|
-
|
|
202
|
+
@config ||= Config.new
|
|
196
203
|
end
|
|
197
204
|
|
|
198
205
|
def self.configure
|
|
@@ -200,7 +207,7 @@ module RESTFramework
|
|
|
200
207
|
end
|
|
201
208
|
|
|
202
209
|
def self.features
|
|
203
|
-
|
|
210
|
+
@features ||= {}
|
|
204
211
|
end
|
|
205
212
|
end
|
|
206
213
|
|