rest_framework 0.7.6 → 0.7.8
Sign up to get free protection for your applications and to get access to all the features.
- 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 +0 -1
- data/lib/rest_framework/controller_mixins/models.rb +82 -46
- data/lib/rest_framework/filters.rb +2 -27
- data/lib/rest_framework/routers.rb +1 -1
- data/lib/rest_framework/serializers.rb +84 -36
- data/lib/rest_framework/utils.rb +10 -6
- 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: e5c7efedb6af2b7a589a2b3f7cedd600f9851bb4ba6be334465285f052e961ed
|
4
|
+
data.tar.gz: e14be403f25acf8e17ba54e324ec7b13e838a28b171e4866fcbb287b4f15a4dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 730bb926137c31d86215cb433c97ea2d5252c24d8b38f8ce78596296a30cfc3786a5b69664b927323dbf9598385ed9b5a79cb931821ee9dad9aa9e7c7d08e111
|
7
|
+
data.tar.gz: 12af341b9c75a9c63b9a48a5ca71aab3dcb521f03862a74acd595d732df1e55ee9d8f16eb34485ad8b640439c4ab28dde08db20f8e598c583c66df3a3a58d121
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.8
|
@@ -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,
|
@@ -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.
|
@@ -389,26 +412,27 @@ module RESTFramework::BaseModelControllerMixin
|
|
389
412
|
# allowed parameters or fields.
|
390
413
|
allowed_params = self.get_allowed_parameters&.map(&:to_s)
|
391
414
|
body_params = if allowed_params
|
392
|
-
data.select { |p|
|
415
|
+
data.select { |p|
|
416
|
+
p.in?(allowed_params) || (
|
417
|
+
self.class.permit_id_assignment && (
|
418
|
+
p.chomp("_id").in?(allowed_params) || p.chomp("_ids").pluralize.in?(allowed_params)
|
419
|
+
)
|
420
|
+
) || (
|
421
|
+
self.class.permit_nested_attributes_assignment &&
|
422
|
+
p.chomp("_attributes").in?(allowed_params)
|
423
|
+
|
424
|
+
)
|
425
|
+
}
|
393
426
|
else
|
394
427
|
data
|
395
428
|
end
|
396
429
|
|
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
430
|
# Filter primary key if configured.
|
407
431
|
if self.class.filter_pk_from_request_body
|
408
432
|
body_params.delete(self.class.get_model&.primary_key)
|
409
433
|
end
|
410
434
|
|
411
|
-
# Filter fields in exclude_body_fields
|
435
|
+
# Filter fields in `exclude_body_fields`.
|
412
436
|
(self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
413
437
|
|
414
438
|
return body_params
|
@@ -430,11 +454,23 @@ module RESTFramework::BaseModelControllerMixin
|
|
430
454
|
return @recordset = nil
|
431
455
|
end
|
432
456
|
|
457
|
+
# Get the recordset but with any associations included to avoid N+1 queries.
|
458
|
+
def get_recordset_with_includes
|
459
|
+
reflections = self.class.get_model.reflections.keys
|
460
|
+
associations = self.get_fields(fallback: true).select { |f| f.in?(reflections) }
|
461
|
+
|
462
|
+
if associations.any?
|
463
|
+
return self.get_recordset.includes(associations)
|
464
|
+
end
|
465
|
+
|
466
|
+
return self.get_recordset
|
467
|
+
end
|
468
|
+
|
433
469
|
# Get the records this controller has access to *after* any filtering is applied.
|
434
470
|
def get_records
|
435
471
|
return @records if instance_variable_defined?(:@records)
|
436
472
|
|
437
|
-
return @records = self.get_filtered_data(self.
|
473
|
+
return @records = self.get_filtered_data(self.get_recordset_with_includes)
|
438
474
|
end
|
439
475
|
|
440
476
|
# Get a single record by primary key or another column, if allowed. The return value is cached and
|
@@ -23,25 +23,18 @@ class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
|
23
23
|
def _get_filter_params
|
24
24
|
# Map filterset fields to strings because query parameter keys are strings.
|
25
25
|
fields = self._get_fields
|
26
|
-
@associations = []
|
27
26
|
|
28
27
|
return @controller.request.query_parameters.select { |p, _|
|
29
28
|
# Remove any trailing `__in` from the field name.
|
30
29
|
field = p.chomp("__in")
|
31
30
|
|
32
|
-
# Remove any associations whose sub-fields are not filterable.
|
33
|
-
# so the caller can include them.
|
31
|
+
# Remove any associations whose sub-fields are not filterable.
|
34
32
|
if match = /(.*)\.(.*)/.match(field)
|
35
33
|
field, sub_field = match[1..2]
|
36
34
|
next false unless field.in?(fields)
|
37
35
|
|
38
36
|
sub_fields = @controller.class.get_field_config(field)[:sub_fields]
|
39
|
-
|
40
|
-
@associations << field.to_sym
|
41
|
-
next true
|
42
|
-
end
|
43
|
-
|
44
|
-
next false
|
37
|
+
next sub_field.in?(sub_fields)
|
45
38
|
end
|
46
39
|
|
47
40
|
next field.in?(fields)
|
@@ -64,9 +57,6 @@ class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
|
64
57
|
# Filter data according to the request query parameters.
|
65
58
|
def get_filtered_data(data)
|
66
59
|
if filter_params = self._get_filter_params.presence
|
67
|
-
# Include any associations.
|
68
|
-
data = data.includes(*@associations) unless @associations.empty?
|
69
|
-
|
70
60
|
return data.where(**filter_params)
|
71
61
|
end
|
72
62
|
|
@@ -88,8 +78,6 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
88
78
|
def _get_ordering
|
89
79
|
return nil if @controller.class.ordering_query_param.blank?
|
90
80
|
|
91
|
-
@associations = []
|
92
|
-
|
93
81
|
# Ensure ordering_fields are strings since the split param will be strings.
|
94
82
|
fields = self._get_fields
|
95
83
|
order_string = @controller.params[@controller.class.ordering_query_param]
|
@@ -106,16 +94,6 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
106
94
|
end
|
107
95
|
next unless !fields || column.in?(fields)
|
108
96
|
|
109
|
-
# Populate any `@associations` so the caller can include them.
|
110
|
-
if match = /(.*)\.(.*)/.match(column)
|
111
|
-
association, sub_field = match[1..2]
|
112
|
-
@associations << association.to_sym
|
113
|
-
|
114
|
-
# Also, due to Rails weirdness, we need to convert the association name to the table name.
|
115
|
-
table_name = @controller.class.get_model.reflections[association].table_name
|
116
|
-
column = "#{table_name}.#{sub_field}"
|
117
|
-
end
|
118
|
-
|
119
97
|
ordering[column] = direction
|
120
98
|
end
|
121
99
|
return ordering
|
@@ -130,9 +108,6 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
130
108
|
reorder = !@controller.class.ordering_no_reorder
|
131
109
|
|
132
110
|
if ordering && !ordering.empty?
|
133
|
-
# Include any associations.
|
134
|
-
data = data.includes(*@associations) unless @associations.empty?
|
135
|
-
|
136
111
|
return data.send(reorder ? :reorder : :order, ordering)
|
137
112
|
end
|
138
113
|
|
@@ -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.
|
@@ -168,8 +168,14 @@ module RESTFramework::Utils
|
|
168
168
|
return model.column_names.reject { |c|
|
169
169
|
c.in?(foreign_keys)
|
170
170
|
} + model.reflections.map { |association, ref|
|
171
|
-
|
172
|
-
|
171
|
+
# Exclude certain associations (by default, active storage and action text associations).
|
172
|
+
if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
|
173
|
+
next nil
|
174
|
+
end
|
175
|
+
|
176
|
+
if ref.collection? && RESTFramework.config.large_reverse_association_tables&.include?(
|
177
|
+
ref.table_name,
|
178
|
+
)
|
173
179
|
next nil
|
174
180
|
end
|
175
181
|
|
@@ -179,9 +185,7 @@ module RESTFramework::Utils
|
|
179
185
|
|
180
186
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
181
187
|
def self.sub_fields_for(ref)
|
182
|
-
model = ref.klass
|
183
|
-
|
184
|
-
if model
|
188
|
+
if !ref.polymorphic? && model = ref.klass
|
185
189
|
sub_fields = [model.primary_key].flatten.compact
|
186
190
|
|
187
191
|
# 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.8
|
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-19 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
|