rest_framework 0.7.7 → 0.7.9
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/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +16 -4
- data/app/views/rest_framework/_form_routes.html.erb +10 -0
- data/app/views/rest_framework/_head.html.erb +15 -5
- data/app/views/rest_framework/_html_form.html.erb +7 -0
- data/app/views/rest_framework/_raw_form.html.erb +1 -10
- data/lib/rest_framework/controller_mixins/base.rb +4 -1
- data/lib/rest_framework/controller_mixins/models.rb +90 -50
- data/lib/rest_framework/routers.rb +1 -1
- data/lib/rest_framework/serializers.rb +84 -36
- data/lib/rest_framework/utils.rb +14 -9
- data/lib/rest_framework.rb +16 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6a9bc78fae632a2c86426c523fbce432cb85904ea661219032d6e4352ab16ea
|
|
4
|
+
data.tar.gz: f202cd11931c54fada9e8dd1e3af1dd9344b2a883dd5cb00ebf45708afdd365c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8759e8295bb59715cd8aab800bd6a158a17422bee4adea2fc5d77c8517d44a92b6ff4a771a2c10a021162f5c582b4853a9d1ebd2bb5cc4e29468f3a6b061460b
|
|
7
|
+
data.tar.gz: 46e0b4693c1166e774f00a25f0f9378f2702e0449f87b2e808ea16d395b1df98ae5e564fff198daf53b6d3e0ba3352a12232a2a7f6962a74ea578b0f9a77c09d
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.7.
|
|
1
|
+
0.7.9
|
|
@@ -126,15 +126,22 @@
|
|
|
126
126
|
Routes
|
|
127
127
|
</a>
|
|
128
128
|
</li>
|
|
129
|
-
<%
|
|
129
|
+
<% @_rrf_form_routes = @route_groups.values[0].select { |r|
|
|
130
130
|
r[:matches_params] && r[:verb].in?(["POST", "PUT", "PATCH"])
|
|
131
131
|
} %>
|
|
132
|
-
<% unless
|
|
132
|
+
<% unless @_rrf_form_routes.empty? %>
|
|
133
133
|
<li class="nav-item">
|
|
134
134
|
<a class="nav-link" href="#tab-raw-form" data-bs-toggle="tab" role="tab">
|
|
135
135
|
Raw Form
|
|
136
136
|
</a>
|
|
137
137
|
</li>
|
|
138
|
+
<% if RESTFramework.features[:html_forms] %>
|
|
139
|
+
<li class="nav-item">
|
|
140
|
+
<a class="nav-link" href="#tab-raw-form" data-bs-toggle="tab" role="tab">
|
|
141
|
+
HTML Form
|
|
142
|
+
</a>
|
|
143
|
+
</li>
|
|
144
|
+
<% end %>
|
|
138
145
|
<% end %>
|
|
139
146
|
</ul>
|
|
140
147
|
</div>
|
|
@@ -142,10 +149,15 @@
|
|
|
142
149
|
<div class="tab-pane fade show active" id="tab-routes" role="tab">
|
|
143
150
|
<%= render partial: 'rest_framework/routes' %>
|
|
144
151
|
</div>
|
|
145
|
-
<% unless
|
|
152
|
+
<% unless @_rrf_form_routes.empty? %>
|
|
146
153
|
<div class="tab-pane fade" id="tab-raw-form" role="tab">
|
|
147
|
-
<%= render partial: 'rest_framework/raw_form'
|
|
154
|
+
<%= render partial: 'rest_framework/raw_form' %>
|
|
148
155
|
</div>
|
|
156
|
+
<% if RESTFramework.features[:html_forms] %>
|
|
157
|
+
<div class="tab-pane fade" id="tab-raw-form" role="tab">
|
|
158
|
+
<%= render partial: 'rest_framework/html_form' %>
|
|
159
|
+
</div>
|
|
160
|
+
<% end %>
|
|
149
161
|
<% end %>
|
|
150
162
|
</div>
|
|
151
163
|
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="mb-2">
|
|
2
|
+
<label class="form-label w-100">Route
|
|
3
|
+
<select class="form-control" id="rawFormRoute">
|
|
4
|
+
<% @_rrf_form_routes.each do |route| %>
|
|
5
|
+
<% path = @route_props[:with_path_args].call(route[:route]) %>
|
|
6
|
+
<option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
|
|
7
|
+
<% end %>
|
|
8
|
+
</select>
|
|
9
|
+
</label>
|
|
10
|
+
</div>
|
|
@@ -73,8 +73,8 @@ code {
|
|
|
73
73
|
// What to do when document loads.
|
|
74
74
|
document.addEventListener("DOMContentLoaded", (event) => {
|
|
75
75
|
// Pretty-print JSON.
|
|
76
|
-
[...document.getElementsByClassName("language-json")].forEach((
|
|
77
|
-
|
|
76
|
+
[...document.getElementsByClassName("language-json")].forEach((el, index) => {
|
|
77
|
+
el.innerHTML = neatJSON(JSON.parse(el.innerText), {
|
|
78
78
|
wrap: 80,
|
|
79
79
|
afterComma: 1,
|
|
80
80
|
afterColon: 1,
|
|
@@ -84,15 +84,25 @@ document.addEventListener("DOMContentLoaded", (event) => {
|
|
|
84
84
|
// Then highlight it.
|
|
85
85
|
hljs.highlightAll();
|
|
86
86
|
|
|
87
|
+
// Replace all text nodes with anchor tag links.
|
|
88
|
+
[...document.querySelectorAll(".rrf-copy code")].forEach((el, index) => {
|
|
89
|
+
el.innerHTML = rrfLinkify(el.innerHTML)
|
|
90
|
+
});
|
|
91
|
+
|
|
87
92
|
// Insert copy link and callback to copy contents of `<code>` element.
|
|
88
|
-
[...document.
|
|
89
|
-
|
|
93
|
+
[...document.querySelectorAll("rrf-copy")].forEach((el, index) => {
|
|
94
|
+
el.insertAdjacentHTML(
|
|
90
95
|
"afterbegin",
|
|
91
96
|
"<a class=\"rrf-copy-link\" onclick=\"return rrfCopyToClipboard(this)\" href=\"#\">Copy to Clipboard</a>",
|
|
92
97
|
)
|
|
93
|
-
})
|
|
98
|
+
});
|
|
94
99
|
})
|
|
95
100
|
|
|
101
|
+
// Convert plain-text links to anchor tag links.
|
|
102
|
+
function rrfLinkify(text) {
|
|
103
|
+
return text.replace(/(https?:\/\/[^\s<>"]+)/g, "<a href=\"$1\" target=\"_blank\">$1</a>")
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
// Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
|
|
97
107
|
function rrfReplaceDocument(content) {
|
|
98
108
|
// Replace the document with provided content.
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
<div style="max-width: 60em; margin: auto">
|
|
2
|
-
|
|
3
|
-
<label class="form-label w-100">Route
|
|
4
|
-
<select class="form-control" id="rawFormRoute">
|
|
5
|
-
<% raw_form_routes.each do |route| %>
|
|
6
|
-
<% path = @route_props[:with_path_args].call(route[:route]) %>
|
|
7
|
-
<option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
|
|
8
|
-
<% end %>
|
|
9
|
-
</select>
|
|
10
|
-
</label>
|
|
11
|
-
</div>
|
|
2
|
+
<%= render partial: "rest_framework/form_routes" %>
|
|
12
3
|
|
|
13
4
|
<div class="mb-2">
|
|
14
5
|
<label class="form-label w-100">Media Type
|
|
@@ -11,7 +11,6 @@ module RESTFramework::BaseControllerMixin
|
|
|
11
11
|
exclude_body_fields: [
|
|
12
12
|
:created_at, :created_by, :created_by_id, :updated_at, :updated_by, :updated_by_id
|
|
13
13
|
].freeze,
|
|
14
|
-
accept_generic_params_as_body_params: false,
|
|
15
14
|
extra_actions: nil,
|
|
16
15
|
extra_member_actions: nil,
|
|
17
16
|
filter_backends: nil,
|
|
@@ -171,6 +170,10 @@ module RESTFramework::BaseControllerMixin
|
|
|
171
170
|
# Handle some common exceptions.
|
|
172
171
|
unless RESTFramework.config.disable_rescue_from
|
|
173
172
|
base.rescue_from(
|
|
173
|
+
ActionController::ParameterMissing,
|
|
174
|
+
ActionController::UnpermittedParameters,
|
|
175
|
+
ActiveRecord::AssociationTypeMismatch,
|
|
176
|
+
ActiveRecord::NotNullViolation,
|
|
174
177
|
ActiveRecord::RecordNotFound,
|
|
175
178
|
ActiveRecord::RecordInvalid,
|
|
176
179
|
ActiveRecord::RecordNotSaved,
|
|
@@ -15,6 +15,11 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
15
15
|
field_config: nil,
|
|
16
16
|
action_fields: nil,
|
|
17
17
|
|
|
18
|
+
# Options for what should be included/excluded from default fields.
|
|
19
|
+
exclude_associations: false,
|
|
20
|
+
include_active_storage: false,
|
|
21
|
+
include_action_text: false,
|
|
22
|
+
|
|
18
23
|
# Attributes for finding records.
|
|
19
24
|
find_by_fields: nil,
|
|
20
25
|
find_by_query_param: "find_by",
|
|
@@ -29,6 +34,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
29
34
|
native_serializer_plural_config: nil,
|
|
30
35
|
native_serializer_only_query_param: "only",
|
|
31
36
|
native_serializer_except_query_param: "except",
|
|
37
|
+
native_serializer_associations_limit: nil,
|
|
38
|
+
native_serializer_associations_limit_query_param: "associations_limit",
|
|
39
|
+
native_serializer_include_associations_count: false,
|
|
32
40
|
|
|
33
41
|
# Attributes for default model filtering, ordering, and searching.
|
|
34
42
|
filterset_fields: nil,
|
|
@@ -39,15 +47,16 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
39
47
|
search_query_param: "search",
|
|
40
48
|
search_ilike: false,
|
|
41
49
|
|
|
50
|
+
# Options for association assignment.
|
|
51
|
+
permit_id_assignment: true,
|
|
52
|
+
permit_nested_attributes_assignment: true,
|
|
53
|
+
|
|
42
54
|
# Option for `recordset.create` vs `Model.create` behavior.
|
|
43
55
|
create_from_recordset: true,
|
|
44
56
|
|
|
45
57
|
# Control if filtering is done before find.
|
|
46
58
|
filter_recordset_before_find: true,
|
|
47
59
|
|
|
48
|
-
# Option to exclude associations from default fields.
|
|
49
|
-
exclude_associations: false,
|
|
50
|
-
|
|
51
60
|
# Control if bulk operations are done in a transaction and rolled back on error, or if all bulk
|
|
52
61
|
# operations are attempted and errors simply returned in the response.
|
|
53
62
|
bulk_transactional: false,
|
|
@@ -91,19 +100,26 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
91
100
|
return self.get_model.human_attribute_name(s, default: super)
|
|
92
101
|
end
|
|
93
102
|
|
|
94
|
-
# Get
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
# Get the available fields. Returning `nil` indicates that anything should be accepted. If
|
|
104
|
+
# `fallback` is true, then we should fallback to this controller's model columns, or an empty
|
|
105
|
+
# array.
|
|
106
|
+
def get_fields(input_fields: nil, fallback: true)
|
|
107
|
+
input_fields ||= self.fields if fallback
|
|
108
|
+
|
|
109
|
+
# If fields is a hash, then parse it.
|
|
110
|
+
if input_fields.is_a?(Hash)
|
|
97
111
|
return RESTFramework::Utils.parse_fields_hash(
|
|
98
|
-
|
|
112
|
+
input_fields, self.get_model, exclude_associations: self.exclude_associations
|
|
99
113
|
)
|
|
114
|
+
elsif !input_fields && fallback
|
|
115
|
+
# Otherwise, if fields is nil and fallback is true, then fallback to columns.
|
|
116
|
+
model = self.get_model
|
|
117
|
+
return model ? RESTFramework::Utils.fields_for(
|
|
118
|
+
model, exclude_associations: self.exclude_associations
|
|
119
|
+
) : []
|
|
100
120
|
end
|
|
101
121
|
|
|
102
|
-
return
|
|
103
|
-
self.get_model ? RESTFramework::Utils.fields_for(
|
|
104
|
-
self.get_model, exclude_associations: self.exclude_associations
|
|
105
|
-
) : []
|
|
106
|
-
)
|
|
122
|
+
return input_fields
|
|
107
123
|
end
|
|
108
124
|
|
|
109
125
|
# Get a field's config, including defaults.
|
|
@@ -112,8 +128,12 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
112
128
|
|
|
113
129
|
# Default sub-fields if field is an association.
|
|
114
130
|
if ref = self.get_model.reflections[f]
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
if ref.polymorphic?
|
|
132
|
+
columns = {}
|
|
133
|
+
else
|
|
134
|
+
model = ref.klass
|
|
135
|
+
columns = model.columns_hash
|
|
136
|
+
end
|
|
117
137
|
config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
|
|
118
138
|
|
|
119
139
|
# Serialize very basic metadata about sub-fields.
|
|
@@ -193,18 +213,37 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
193
213
|
# Get association metadata.
|
|
194
214
|
if ref = reflections[f]
|
|
195
215
|
metadata[:kind] = "association"
|
|
216
|
+
|
|
217
|
+
# Determine if we render id/ids fields.
|
|
218
|
+
if self.permit_id_assignment
|
|
219
|
+
if ref.collection?
|
|
220
|
+
metadata[:id_field] = "#{f.singularize}_ids"
|
|
221
|
+
else
|
|
222
|
+
metadata[:id_field] = "#{f}_id"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Determine if we render nested attributes options.
|
|
227
|
+
if self.permit_nested_attributes_assignment
|
|
228
|
+
if nested_opts = model.nested_attributes_options[f.to_sym].presence
|
|
229
|
+
nested_opts[:field] = "#{f}_attributes"
|
|
230
|
+
metadata[:nested_attributes_options] = nested_opts
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
196
234
|
begin
|
|
197
235
|
pk = ref.active_record_primary_key
|
|
198
236
|
rescue ActiveRecord::UnknownPrimaryKey
|
|
199
237
|
end
|
|
200
238
|
metadata[:association] = {
|
|
201
239
|
macro: ref.macro,
|
|
240
|
+
collection: ref.collection?,
|
|
202
241
|
class_name: ref.class_name,
|
|
203
242
|
foreign_key: ref.foreign_key,
|
|
204
243
|
primary_key: pk,
|
|
205
244
|
polymorphic: ref.polymorphic?,
|
|
206
245
|
table_name: ref.table_name,
|
|
207
|
-
options: ref.options.presence,
|
|
246
|
+
options: ref.options.as_json.presence,
|
|
208
247
|
}.compact
|
|
209
248
|
end
|
|
210
249
|
|
|
@@ -327,26 +366,10 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
327
366
|
return (action_config[action] if action) || self.class.send(generic_config_key)
|
|
328
367
|
end
|
|
329
368
|
|
|
330
|
-
# Get a list of fields
|
|
331
|
-
# accepted unless `fallback` is true, in which case we should fallback to this controller's model
|
|
332
|
-
# columns, or en empty array.
|
|
369
|
+
# Get a list of fields, taking into account the current action.
|
|
333
370
|
def get_fields(fallback: false)
|
|
334
|
-
fields = _get_specific_action_config(:action_fields, :fields)
|
|
335
|
-
|
|
336
|
-
# If fields is a hash, then parse it.
|
|
337
|
-
if fields.is_a?(Hash)
|
|
338
|
-
return RESTFramework::Utils.parse_fields_hash(
|
|
339
|
-
fields, self.class.get_model, exclude_associations: self.class.exclude_associations
|
|
340
|
-
)
|
|
341
|
-
elsif !fields && fallback
|
|
342
|
-
# Otherwise, if fields is nil and fallback is true, then fallback to columns.
|
|
343
|
-
model = self.class.get_model
|
|
344
|
-
return model ? RESTFramework::Utils.fields_for(
|
|
345
|
-
model, exclude_associations: self.class.exclude_associations
|
|
346
|
-
) : []
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
return fields
|
|
371
|
+
fields = self._get_specific_action_config(:action_fields, :fields)
|
|
372
|
+
return self.class.get_fields(input_fields: fields, fallback: fallback)
|
|
350
373
|
end
|
|
351
374
|
|
|
352
375
|
# Pass fields to get dynamic metadata based on which fields are available.
|
|
@@ -363,10 +386,36 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
363
386
|
# Get a list of parameters allowed for the current action. By default we do not fallback to
|
|
364
387
|
# columns so arbitrary fields can be submitted if no fields are defined.
|
|
365
388
|
def get_allowed_parameters
|
|
366
|
-
return
|
|
389
|
+
return @allowed_parameters if defined?(@allowed_parameters)
|
|
390
|
+
|
|
391
|
+
@allowed_parameters = self._get_specific_action_config(
|
|
367
392
|
:allowed_action_parameters,
|
|
368
393
|
:allowed_parameters,
|
|
369
|
-
)
|
|
394
|
+
)
|
|
395
|
+
return @allowed_parameters if @allowed_parameters
|
|
396
|
+
return @allowed_parameters = nil unless fields = self.get_fields
|
|
397
|
+
|
|
398
|
+
# For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
|
|
399
|
+
return @allowed_parameters = fields.map { |f|
|
|
400
|
+
f = f.to_s
|
|
401
|
+
next f unless ref = self.class.get_model.reflections[f]
|
|
402
|
+
|
|
403
|
+
variations = [f]
|
|
404
|
+
|
|
405
|
+
if self.class.permit_id_assignment
|
|
406
|
+
if ref.collection?
|
|
407
|
+
variations << "#{f.singularize}_ids"
|
|
408
|
+
else
|
|
409
|
+
variations << "#{f}_id"
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
if self.class.permit_nested_attributes_assignment
|
|
414
|
+
variations << "#{f}_attributes"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
next variations
|
|
418
|
+
}.flatten
|
|
370
419
|
end
|
|
371
420
|
|
|
372
421
|
# Get the configured serializer class, or `NativeSerializer` as a default.
|
|
@@ -381,34 +430,25 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
381
430
|
]
|
|
382
431
|
end
|
|
383
432
|
|
|
384
|
-
#
|
|
433
|
+
# Use strong parameters to filter the request body using the configured allowed parameters.
|
|
385
434
|
def get_body_params(data: nil)
|
|
386
435
|
data ||= request.request_parameters
|
|
387
436
|
|
|
388
437
|
# Filter the request body and map to strings. Return all params if we cannot resolve a list of
|
|
389
438
|
# allowed parameters or fields.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
data.
|
|
439
|
+
body_params = if allowed_parameters = self.get_allowed_parameters
|
|
440
|
+
data = ActionController::Parameters.new(data)
|
|
441
|
+
data.permit(*allowed_parameters)
|
|
393
442
|
else
|
|
394
443
|
data
|
|
395
444
|
end
|
|
396
445
|
|
|
397
|
-
# Add query params in place of missing body params, if configured.
|
|
398
|
-
if self.class.accept_generic_params_as_body_params && allowed_params
|
|
399
|
-
(allowed_params - body_params.keys).each do |k|
|
|
400
|
-
if value = params[k].presence
|
|
401
|
-
body_params[k] = value
|
|
402
|
-
end
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
|
|
406
446
|
# Filter primary key if configured.
|
|
407
447
|
if self.class.filter_pk_from_request_body
|
|
408
448
|
body_params.delete(self.class.get_model&.primary_key)
|
|
409
449
|
end
|
|
410
450
|
|
|
411
|
-
# Filter fields in exclude_body_fields
|
|
451
|
+
# Filter fields in `exclude_body_fields`.
|
|
412
452
|
(self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
|
413
453
|
|
|
414
454
|
return body_params
|
|
@@ -157,7 +157,7 @@ module ActionDispatch::Routing
|
|
|
157
157
|
public_send(:resource, name, only: [], **kwargs) do
|
|
158
158
|
# Route a root for this resource.
|
|
159
159
|
if route_root_to
|
|
160
|
-
get("", action: route_root_to)
|
|
160
|
+
get("", action: route_root_to, as: "")
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
collection do
|
|
@@ -192,6 +192,83 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
192
192
|
return cfg
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
+
# Get the associations limit from the controller.
|
|
196
|
+
def _get_associations_limit
|
|
197
|
+
return @_get_associations_limit if defined?(@_get_associations_limit)
|
|
198
|
+
|
|
199
|
+
limit = @controller.class.native_serializer_associations_limit
|
|
200
|
+
|
|
201
|
+
# Extract the limit from the query parameters if it's set.
|
|
202
|
+
if query_param = @controller.class.native_serializer_associations_limit_query_param
|
|
203
|
+
if @controller.request.query_parameters.key?(query_param)
|
|
204
|
+
query_limit = @controller.request.query_parameters[query_param].to_i
|
|
205
|
+
if query_limit > 0
|
|
206
|
+
limit = query_limit
|
|
207
|
+
else
|
|
208
|
+
limit = nil
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
return @_get_associations_limit = limit
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Get a serializer configuration from the controller. `@controller` and `@model` must be set.
|
|
217
|
+
def _get_controller_serializer_config(fields)
|
|
218
|
+
columns = []
|
|
219
|
+
includes = {}
|
|
220
|
+
methods = []
|
|
221
|
+
serializer_methods = {}
|
|
222
|
+
fields.each do |f|
|
|
223
|
+
if f.in?(@model.column_names)
|
|
224
|
+
columns << f
|
|
225
|
+
elsif ref = @model.reflections[f]
|
|
226
|
+
sub_columns = []
|
|
227
|
+
sub_methods = []
|
|
228
|
+
@controller.class.get_field_config(f)[:sub_fields].each do |sf|
|
|
229
|
+
if !ref.polymorphic? && sf.in?(ref.klass.column_names)
|
|
230
|
+
sub_columns << sf
|
|
231
|
+
else
|
|
232
|
+
sub_methods << sf
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
sub_config = {only: sub_columns, methods: sub_methods}
|
|
236
|
+
|
|
237
|
+
# Apply certain rules regarding collection associations.
|
|
238
|
+
if ref.collection?
|
|
239
|
+
# If we need to limit the number of serialized association records, then dynamically add a
|
|
240
|
+
# serializer method to do so.
|
|
241
|
+
if limit = self._get_associations_limit
|
|
242
|
+
method_name = "__rrf_limit_method_#{f}"
|
|
243
|
+
serializer_methods[method_name] = f
|
|
244
|
+
self.define_singleton_method(method_name) do |record|
|
|
245
|
+
next record.send(f).limit(limit).as_json(**sub_config)
|
|
246
|
+
end
|
|
247
|
+
else
|
|
248
|
+
includes[f] = sub_config
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# If we need to include the association count, then add it here.
|
|
252
|
+
if @controller.class.native_serializer_include_associations_count
|
|
253
|
+
method_name = "__rrf_count_method_#{f}"
|
|
254
|
+
serializer_methods[method_name] = "#{f}.count"
|
|
255
|
+
self.define_singleton_method(method_name) do |record|
|
|
256
|
+
next record.send(f).count
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
else
|
|
260
|
+
includes[f] = sub_config
|
|
261
|
+
end
|
|
262
|
+
elsif @model.method_defined?(f)
|
|
263
|
+
methods << f
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
only: columns, include: includes, methods: methods, serializer_methods: serializer_methods
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
|
|
195
272
|
# Get the raw serializer config. Use `deep_dup` on any class mutables (array, hash, etc) to avoid
|
|
196
273
|
# mutating class state.
|
|
197
274
|
def _get_raw_serializer_config
|
|
@@ -206,40 +283,11 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
206
283
|
end
|
|
207
284
|
|
|
208
285
|
# If the config wasn't determined, build a serializer config from controller fields.
|
|
209
|
-
if fields = @controller&.get_fields(fallback: true)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
columns = []
|
|
213
|
-
includes = {}
|
|
214
|
-
methods = []
|
|
215
|
-
if @model
|
|
216
|
-
fields.each do |f|
|
|
217
|
-
if f.in?(@model.column_names)
|
|
218
|
-
columns << f
|
|
219
|
-
elsif @model.reflections.key?(f)
|
|
220
|
-
sub_columns = []
|
|
221
|
-
sub_methods = []
|
|
222
|
-
@controller.class.get_field_config(f)[:sub_fields].each do |sf|
|
|
223
|
-
sub_model = @model.reflections[f].klass
|
|
224
|
-
if sf.in?(sub_model.column_names)
|
|
225
|
-
sub_columns << sf
|
|
226
|
-
elsif sub_model.method_defined?(sf)
|
|
227
|
-
sub_methods << sf
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
includes[f] = {only: sub_columns, methods: sub_methods}
|
|
231
|
-
elsif @model.method_defined?(f)
|
|
232
|
-
methods << f
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
else
|
|
236
|
-
columns = fields
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
return {only: columns, include: includes, methods: methods}
|
|
286
|
+
if @model && fields = @controller&.get_fields(fallback: true)
|
|
287
|
+
return self._get_controller_serializer_config(fields.deep_dup)
|
|
240
288
|
end
|
|
241
289
|
|
|
242
|
-
# By default, pass an empty configuration,
|
|
290
|
+
# By default, pass an empty configuration, using the default Rails serializer.
|
|
243
291
|
return {}
|
|
244
292
|
end
|
|
245
293
|
|
|
@@ -250,14 +298,14 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
250
298
|
|
|
251
299
|
# Serialize a single record and merge results of `serializer_methods`.
|
|
252
300
|
def _serialize(record, config, serializer_methods)
|
|
253
|
-
# Ensure serializer_methods is either falsy, or
|
|
254
|
-
if serializer_methods && !serializer_methods.
|
|
255
|
-
serializer_methods = [serializer_methods]
|
|
301
|
+
# Ensure serializer_methods is either falsy, or a hash.
|
|
302
|
+
if serializer_methods && !serializer_methods.is_a?(Hash)
|
|
303
|
+
serializer_methods = [serializer_methods].flatten.map { |m| [m, m] }.to_h
|
|
256
304
|
end
|
|
257
305
|
|
|
258
306
|
# Merge serialized record with any serializer method results.
|
|
259
307
|
return record.serializable_hash(config).merge(
|
|
260
|
-
serializer_methods&.map { |m| [
|
|
308
|
+
serializer_methods&.map { |m, k| [k.to_sym, self.send(m, record)] }.to_h,
|
|
261
309
|
)
|
|
262
310
|
end
|
|
263
311
|
|
data/lib/rest_framework/utils.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module RESTFramework::Utils
|
|
2
2
|
HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
|
|
3
|
-
LABEL_FIELDS = %w(name label login title email username)
|
|
3
|
+
LABEL_FIELDS = %w(name label login title email username url)
|
|
4
4
|
|
|
5
5
|
# Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}`, and
|
|
6
6
|
# additional metadata fields.
|
|
@@ -79,6 +79,7 @@ module RESTFramework::Utils
|
|
|
79
79
|
def self.get_routes(application_routes, request, current_route: nil)
|
|
80
80
|
current_route ||= self.get_request_route(application_routes, request)
|
|
81
81
|
current_path = current_route.path.spec.to_s.gsub("(.:format)", "")
|
|
82
|
+
current_path = "" if current_path == "/"
|
|
82
83
|
current_levels = current_path.count("/")
|
|
83
84
|
current_comparable_path = %r{^#{Regexp.quote(self.comparable_path(current_path))}(/|$)}
|
|
84
85
|
|
|
@@ -112,7 +113,7 @@ module RESTFramework::Utils
|
|
|
112
113
|
verb: r.verb,
|
|
113
114
|
path: path,
|
|
114
115
|
# Starts at the number of levels in current path, and removes the `(.:format)` at the end.
|
|
115
|
-
relative_path: path.split("/")[current_levels..]&.join("/"),
|
|
116
|
+
relative_path: path.split("/")[current_levels..]&.join("/").presence || "/",
|
|
116
117
|
controller: r.defaults[:controller].presence,
|
|
117
118
|
action: r.defaults[:action].presence,
|
|
118
119
|
matches_path: matches_path,
|
|
@@ -125,8 +126,8 @@ module RESTFramework::Utils
|
|
|
125
126
|
# by the path, and finally by the HTTP verb.
|
|
126
127
|
[r[:_levels], r[:_path], HTTP_METHOD_ORDERING.index(r[:verb]) || 99]
|
|
127
128
|
}.group_by { |r| r[:controller] }.sort_by { |c, _r|
|
|
128
|
-
# Sort the controller groups by current controller first, then
|
|
129
|
-
[request.params[:controller] == c ? 0 : 1, c
|
|
129
|
+
# Sort the controller groups by current controller first, then alphanumerically.
|
|
130
|
+
[request.params[:controller] == c ? 0 : 1, c]
|
|
130
131
|
}.to_h
|
|
131
132
|
end
|
|
132
133
|
|
|
@@ -168,8 +169,14 @@ module RESTFramework::Utils
|
|
|
168
169
|
return model.column_names.reject { |c|
|
|
169
170
|
c.in?(foreign_keys)
|
|
170
171
|
} + model.reflections.map { |association, ref|
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
# Exclude certain associations (by default, active storage and action text associations).
|
|
173
|
+
if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
|
|
174
|
+
next nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if ref.collection? && RESTFramework.config.large_reverse_association_tables&.include?(
|
|
178
|
+
ref.table_name,
|
|
179
|
+
)
|
|
173
180
|
next nil
|
|
174
181
|
end
|
|
175
182
|
|
|
@@ -179,9 +186,7 @@ module RESTFramework::Utils
|
|
|
179
186
|
|
|
180
187
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
|
181
188
|
def self.sub_fields_for(ref)
|
|
182
|
-
model = ref.klass
|
|
183
|
-
|
|
184
|
-
if model
|
|
189
|
+
if !ref.polymorphic? && model = ref.klass
|
|
185
190
|
sub_fields = [model.primary_key].flatten.compact
|
|
186
191
|
|
|
187
192
|
# Preferrably find a database column to use as label.
|
data/lib/rest_framework.rb
CHANGED
|
@@ -21,6 +21,12 @@ module RESTFramework
|
|
|
21
21
|
# Global configuration should be kept minimal, as controller-level configurations allows multiple
|
|
22
22
|
# APIs to be defined to behave differently.
|
|
23
23
|
class Config
|
|
24
|
+
DEFAULT_EXCLUDE_ASSOCIATION_CLASSES = %w(
|
|
25
|
+
ActionText::RichText
|
|
26
|
+
ActiveStorage::Attachment
|
|
27
|
+
ActiveStorage::Blob
|
|
28
|
+
).freeze
|
|
29
|
+
|
|
24
30
|
# Do not run `rrf_finalize` on controllers automatically using a `TracePoint` hook. This is a
|
|
25
31
|
# performance option and must be global because we have to determine this before any
|
|
26
32
|
# controller-specific configuration is set. If this is set to `true`, then you must manually
|
|
@@ -44,8 +50,12 @@ module RESTFramework
|
|
|
44
50
|
# Option to disable `rescue_from` on the controller mixins.
|
|
45
51
|
attr_accessor :disable_rescue_from
|
|
46
52
|
|
|
53
|
+
# Options to exclude certain classes from being added by default as association fields.
|
|
54
|
+
attr_accessor :exclude_association_classes
|
|
55
|
+
|
|
47
56
|
def initialize
|
|
48
57
|
self.show_backtrace = Rails.env.development?
|
|
58
|
+
self.exclude_association_classes = DEFAULT_EXCLUDE_ASSOCIATION_CLASSES
|
|
49
59
|
end
|
|
50
60
|
end
|
|
51
61
|
|
|
@@ -56,6 +66,12 @@ module RESTFramework
|
|
|
56
66
|
def self.configure
|
|
57
67
|
yield(self.config)
|
|
58
68
|
end
|
|
69
|
+
|
|
70
|
+
def self.features
|
|
71
|
+
return @features ||= {
|
|
72
|
+
html_forms: false,
|
|
73
|
+
}
|
|
74
|
+
end
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
require_relative "rest_framework/controller_mixins"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rest_framework
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gregory N. Schmit
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-01-
|
|
11
|
+
date: 2023-01-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -35,7 +35,9 @@ files:
|
|
|
35
35
|
- README.md
|
|
36
36
|
- VERSION
|
|
37
37
|
- app/views/layouts/rest_framework.html.erb
|
|
38
|
+
- app/views/rest_framework/_form_routes.html.erb
|
|
38
39
|
- app/views/rest_framework/_head.html.erb
|
|
40
|
+
- app/views/rest_framework/_html_form.html.erb
|
|
39
41
|
- app/views/rest_framework/_raw_form.html.erb
|
|
40
42
|
- app/views/rest_framework/_route.html.erb
|
|
41
43
|
- app/views/rest_framework/_routes.html.erb
|