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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edeb7963dbca0185ff384e628a185c83341e3154d6414c9d8f70a05115abd23a
4
- data.tar.gz: 8d6bc94fea07e57c20625c8aa192ced88789e078bf3a4b29b71e6c80099f2436
3
+ metadata.gz: e5c7efedb6af2b7a589a2b3f7cedd600f9851bb4ba6be334465285f052e961ed
4
+ data.tar.gz: e14be403f25acf8e17ba54e324ec7b13e838a28b171e4866fcbb287b4f15a4dd
5
5
  SHA512:
6
- metadata.gz: 68ecf8a8a045c3e217c597f86b46573dc80bb931e20be45af01b939079f8cf2dd27d68e8bebd8d458726b04b8b55fb2dd65a60bcf4c847c30feb1257f7cfb9ea
7
- data.tar.gz: 388478f7c8f2c51a96740f25043aa7d188b5e59731478f4bca2ce6171fadc5ba1e201b12ee051859386ce151c1114b9b6f207dbe840e732dec652d9639323825
6
+ metadata.gz: 730bb926137c31d86215cb433c97ea2d5252c24d8b38f8ce78596296a30cfc3786a5b69664b927323dbf9598385ed9b5a79cb931821ee9dad9aa9e7c7d08e111
7
+ data.tar.gz: 12af341b9c75a9c63b9a48a5ca71aab3dcb521f03862a74acd595d732df1e55ee9d8f16eb34485ad8b640439c4ab28dde08db20f8e598c583c66df3a3a58d121
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.6
1
+ 0.7.8
@@ -126,15 +126,22 @@
126
126
  Routes
127
127
  </a>
128
128
  </li>
129
- <% raw_form_routes = @route_groups.values[0].select { |r|
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 raw_form_routes.empty? %>
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 raw_form_routes.empty? %>
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', locals: {raw_form_routes: raw_form_routes} %>
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((element, index) => {
77
- element.innerHTML = neatJSON(JSON.parse(element.innerText), {
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.getElementsByClassName("rrf-copy")].forEach((element, index) => {
89
- element.insertAdjacentHTML(
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.
@@ -0,0 +1,7 @@
1
+ <div style="max-width: 60em; margin: auto">
2
+ <%= render partial: "rest_framework/form_routes" %>
3
+
4
+ <%= form_for @ do |f| %>
5
+ <%= f.file_field :picture %>
6
+ <% end %>
7
+ </div>
@@ -1,14 +1,5 @@
1
1
  <div style="max-width: 60em; margin: auto">
2
- <div class="mb-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 fields without any action context. Always fallback to columns at the class level.
95
- def get_fields
96
- if self.fields.is_a?(Hash)
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
- self.fields, self.get_model, exclude_associations: self.exclude_associations
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 self.fields || (
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
- model = ref.klass
116
- columns = model.columns_hash
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 for the current action. Returning `nil` indicates that anything should be
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| allowed_params.include?(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.get_recordset)
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. Also populate `@associations`
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
- if sub_field.in?(sub_fields)
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
- fields = fields.deep_dup
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, allowing the serialization of all columns.
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 an array.
254
- if serializer_methods && !serializer_methods.respond_to?(:to_ary)
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| [m.to_sym, self.send(m, record)] }.to_h,
308
+ serializer_methods&.map { |m, k| [k.to_sym, self.send(m, record)] }.to_h,
261
309
  )
262
310
  end
263
311
 
@@ -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
- if ref.macro.in?([:has_many, :has_and_belongs_to_many]) &&
172
- RESTFramework.config.large_reverse_association_tables&.include?(ref.table_name)
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.
@@ -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.6
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-15 00:00:00.000000000 Z
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