rest_framework 0.9.2 → 0.9.3

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/views/rest_framework/_external.html.erb +13 -0
  4. data/app/views/rest_framework/_head.html.erb +2 -192
  5. data/{docs/assets/js/rest_framework.js → app/views/rest_framework/_shared.html} +69 -42
  6. data/docs/Gemfile +1 -0
  7. data/docs/Gemfile.lock +14 -14
  8. data/docs/_config.yml +4 -2
  9. data/docs/_guide/2_controllers.md +342 -0
  10. data/docs/_guide/3_serializers.md +1 -1
  11. data/docs/_guide/4_filtering_and_ordering.md +8 -8
  12. data/docs/_includes/external.html +9 -0
  13. data/docs/_includes/head.html +135 -15
  14. data/docs/_includes/shared.html +164 -0
  15. data/lib/rest_framework/controller_mixins/base.rb +23 -36
  16. data/lib/rest_framework/controller_mixins/models.rb +86 -75
  17. data/lib/rest_framework/controller_mixins.rb +1 -0
  18. data/lib/rest_framework/engine.rb +9 -0
  19. data/lib/rest_framework/filters/base.rb +9 -0
  20. data/lib/rest_framework/filters/model_ordering.rb +48 -0
  21. data/lib/rest_framework/filters/model_query.rb +51 -0
  22. data/lib/rest_framework/filters/model_search.rb +41 -0
  23. data/lib/rest_framework/filters/ransack.rb +25 -0
  24. data/lib/rest_framework/filters.rb +6 -150
  25. data/lib/rest_framework/paginators.rb +7 -11
  26. data/lib/rest_framework/serializers.rb +10 -10
  27. data/lib/rest_framework/utils.rb +15 -7
  28. data/lib/rest_framework.rb +93 -4
  29. data/vendor/assets/javascripts/rest_framework/bootstrap.js +7 -0
  30. data/vendor/assets/javascripts/rest_framework/highlight-json.js +7 -0
  31. data/vendor/assets/javascripts/rest_framework/highlight-xml.js +29 -0
  32. data/vendor/assets/javascripts/rest_framework/highlight.js +1202 -0
  33. data/vendor/assets/javascripts/rest_framework/neatjson.js +8 -0
  34. data/vendor/assets/javascripts/rest_framework/trix.js +6 -0
  35. data/vendor/assets/stylesheets/rest_framework/bootstrap-icons.css +13 -0
  36. data/vendor/assets/stylesheets/rest_framework/bootstrap.css +6 -0
  37. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-dark.css +7 -0
  38. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-light.css +7 -0
  39. data/vendor/assets/stylesheets/rest_framework/trix.css +410 -0
  40. metadata +23 -5
  41. data/docs/_guide/2_controller_mixins.md +0 -293
  42. data/docs/assets/css/rest_framework.css +0 -159
@@ -0,0 +1,164 @@
1
+ <!--
2
+ AUTOGENERATED
3
+ Updates must be written to `shared.{css,js}` and synced with `rake maintain_assets`.
4
+ -->
5
+ <style>
6
+ :root {
7
+ --rrf-red: #900;
8
+ --rrf-red-hover: #5f0c0c;
9
+ --rrf-light-red: #db2525;
10
+ --rrf-light-red-hover: #b80404;
11
+ }
12
+ #rrfAccentBar {
13
+ background-color: var(--rrf-red);
14
+ height: .3em;
15
+ }
16
+ header nav { background-color: black; }
17
+
18
+ /* Header adjustments. */
19
+ h1 { font-size: 2rem; }
20
+ h2 { font-size: 1.7rem; }
21
+ h3 { font-size: 1.5rem; }
22
+ h4 { font-size: 1.3rem; }
23
+ h5 { font-size: 1.1rem; }
24
+ h6 { font-size: 1rem; }
25
+ h1, h2, h3, h4, h5, h6 {
26
+ color: var(--rrf-red);
27
+ }
28
+ html[data-bs-theme="dark"] h1,
29
+ html[data-bs-theme="dark"] h2,
30
+ html[data-bs-theme="dark"] h3,
31
+ html[data-bs-theme="dark"] h4,
32
+ html[data-bs-theme="dark"] h5,
33
+ html[data-bs-theme="dark"] h6 {
34
+ color: var(--rrf-light-red);
35
+ }
36
+
37
+ /* Improve code and code blocks. */
38
+ pre code, .trix-content pre {
39
+ display: block;
40
+ overflow-x: auto;
41
+ padding: .5em !important;
42
+ }
43
+ code, .trix-content pre {
44
+ --bs-code-color: black;
45
+ background-color: #eee !important;
46
+ border: 1px solid #aaa;
47
+ border-radius: 3px;
48
+ padding: .1em .3em;
49
+ }
50
+ html[data-bs-theme="dark"] code, html[data-bs-theme="dark"] .trix-content pre {
51
+ --bs-code-color: white;
52
+ background-color: #2b2b2b !important;
53
+ }
54
+
55
+ /* Anchors */
56
+ a:not(.nav-link) {
57
+ text-decoration: none;
58
+ color: var(--rrf-red);
59
+ }
60
+ a:hover:not(.nav-link) {
61
+ text-decoration: underline;
62
+ color: var(--rrf-red-hover);
63
+ }
64
+ html[data-bs-theme="dark"] a:not(.nav-link) { color: var(--rrf-light-red); }
65
+ html[data-bs-theme="dark"] a:hover:not(.nav-link) { color: var(--rrf-light-red-hover); }
66
+
67
+ </style>
68
+ <script>
69
+ ;(() => {
70
+ // Get the real mode from a selected mode. Anything other than "light" or "dark" is treated as
71
+ // "system" mode.
72
+ const rrfGetRealMode = (selectedMode) => {
73
+ if (selectedMode === "light" || selectedMode === "dark") {
74
+ return selectedMode
75
+ }
76
+
77
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
78
+ return "dark"
79
+ }
80
+
81
+ return "light"
82
+ }
83
+
84
+ // Set the mode, given a "selected" mode.
85
+ const rrfSetSelectedMode = (selectedMode) => {
86
+ // Anything except "light" or "dark" is casted to "system".
87
+ if (selectedMode !== "light" && selectedMode !== "dark") {
88
+ selectedMode = "system"
89
+ }
90
+
91
+ // Store selected mode in `localStorage`.
92
+ localStorage.setItem("rrfMode", selectedMode)
93
+
94
+ // Set the mode selector to the selected mode.
95
+ const modeComponent = document.getElementById("rrfModeComponent")
96
+ if (modeComponent) {
97
+ let labelHTML
98
+ modeComponent.querySelectorAll("button[data-rrf-mode-value]").forEach((el) => {
99
+ if (el.getAttribute("data-rrf-mode-value") === selectedMode) {
100
+ el.classList.add("active")
101
+ labelHTML = el.querySelector("i").outerHTML.replace("ms-2", "me-1")
102
+ } else {
103
+ el.classList.remove("active")
104
+ }
105
+ })
106
+ modeComponent.querySelector("button[data-bs-toggle]").innerHTML = labelHTML
107
+ }
108
+
109
+ // Get the real mode to use.
110
+ realMode = rrfGetRealMode(selectedMode)
111
+
112
+ // Set the `realMode` effects.
113
+ if (realMode === "light") {
114
+ document.querySelectorAll(".rrf-light-mode").forEach((el) => {
115
+ el.disabled = false
116
+ })
117
+ document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
118
+ el.disabled = true
119
+ })
120
+ document.querySelectorAll(".rrf-mode").forEach((el) => {
121
+ el.setAttribute("data-bs-theme", "light")
122
+ })
123
+ } else if (realMode === "dark") {
124
+ document.querySelectorAll(".rrf-light-mode").forEach((el) => {
125
+ el.disabled = true
126
+ })
127
+ document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
128
+ el.disabled = false
129
+ })
130
+ document.querySelectorAll(".rrf-mode").forEach((el) => {
131
+ el.setAttribute("data-bs-theme", "dark")
132
+ })
133
+ } else {
134
+ console.log(`RRF: Unknown mode: ${mode}`)
135
+ }
136
+ }
137
+
138
+ // Initialize dark/light mode before page fully loads to prevent flash.
139
+ rrfSetSelectedMode(localStorage.getItem("rrfMode"))
140
+
141
+ // Initialize dark/light mode after page load (mostly so mode component is updated).
142
+ document.addEventListener("DOMContentLoaded", (event) => {
143
+ rrfSetSelectedMode(localStorage.getItem("rrfMode"))
144
+
145
+ // Also set up mode selector.
146
+ document.querySelectorAll("#rrfModeComponent button[data-rrf-mode-value]").forEach((el) => {
147
+ el.addEventListener("click", (event) => {
148
+ rrfSetSelectedMode(event.target.getAttribute("data-rrf-mode-value"))
149
+ })
150
+ })
151
+ })
152
+
153
+ // Handle case where user changes system theme.
154
+ if (window.matchMedia) {
155
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
156
+ const selectedMode = localStorage.getItem("rrfMode")
157
+ if (selectedMode !== "light" && selectedMode !== "dark") {
158
+ rrfSetSelectedMode("system")
159
+ }
160
+ })
161
+ }
162
+ })()
163
+
164
+ </script>
@@ -6,28 +6,18 @@ require_relative "../utils"
6
6
  # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
7
7
  # is defined.
8
8
  module RESTFramework::BaseControllerMixin
9
- RRF_BASE_CONTROLLER_CONFIG = {
10
- filter_pk_from_request_body: true,
11
- exclude_body_fields: %w[
12
- created_at
13
- created_by
14
- created_by_id
15
- updated_at
16
- updated_by
17
- updated_by_id
18
- _method
19
- utf8
20
- authenticity_token
21
- ].freeze,
9
+ RRF_BASE_CONFIG = {
22
10
  extra_actions: nil,
23
11
  extra_member_actions: nil,
24
- filter_backends: nil,
25
12
  singleton_controller: nil,
26
13
 
27
14
  # Options related to metadata and display.
28
15
  title: nil,
29
16
  description: nil,
30
17
  inflect_acronyms: ["ID", "IDs", "REST", "API", "APIs"].freeze,
18
+ }
19
+ RRF_BASE_INSTANCE_CONFIG = {
20
+ filter_backends: nil,
31
21
 
32
22
  # Options related to serialization.
33
23
  rescue_unknown_format_with: :json,
@@ -42,10 +32,6 @@ module RESTFramework::BaseControllerMixin
42
32
  page_size_query_param: "page_size",
43
33
  max_page_size: nil,
44
34
 
45
- # Options related to bulk actions and batch processing.
46
- bulk_guard_query_param: nil,
47
- enable_batch_processing: nil,
48
-
49
35
  # Option to disable serializer adapters by default, mainly introduced because Active Model
50
36
  # Serializers will do things like serialize `[]` into `{"":[]}`.
51
37
  disable_adapters_by_default: true,
@@ -146,7 +132,7 @@ module RESTFramework::BaseControllerMixin
146
132
  # :nocov:
147
133
  def rrf_finalize
148
134
  if RESTFramework.config.freeze_config
149
- self::RRF_BASE_CONTROLLER_CONFIG.keys.each { |k|
135
+ (self::RRF_BASE_CONFIG.keys + self::RRF_BASE_INSTANCE_CONFIG.keys).each { |k|
150
136
  v = self.send(k)
151
137
  v.freeze if v.is_a?(Hash) || v.is_a?(Array)
152
138
  }
@@ -161,14 +147,15 @@ module RESTFramework::BaseControllerMixin
161
147
  base.extend(ClassMethods)
162
148
 
163
149
  # Add class attributes (with defaults) unless they already exist.
164
- RRF_BASE_CONTROLLER_CONFIG.each do |a, default|
150
+ RRF_BASE_CONFIG.each do |a, default|
165
151
  next if base.respond_to?(a)
166
152
 
167
- base.class_attribute(a)
153
+ base.class_attribute(a, default: default, instance_accessor: false)
154
+ end
155
+ RRF_BASE_INSTANCE_CONFIG.each do |a, default|
156
+ next if base.respond_to?(a)
168
157
 
169
- # Set default manually so we can still support Rails 4. Maybe later we can use the default
170
- # parameter on `class_attribute`.
171
- base.send(:"#{a}=", default)
158
+ base.class_attribute(a, default: default)
172
159
  end
173
160
 
174
161
  # Alias `extra_actions` to `extra_collection_actions`.
@@ -219,7 +206,7 @@ module RESTFramework::BaseControllerMixin
219
206
 
220
207
  # Get the configured serializer class.
221
208
  def get_serializer_class
222
- return nil unless serializer_class = self.class.serializer_class
209
+ return nil unless serializer_class = self.serializer_class
223
210
 
224
211
  # Support dynamically resolving serializer given a symbol or string.
225
212
  serializer_class = serializer_class.to_s if serializer_class.is_a?(Symbol)
@@ -242,7 +229,7 @@ module RESTFramework::BaseControllerMixin
242
229
 
243
230
  # Get filtering backends, defaulting to no backends.
244
231
  def get_filter_backends
245
- return self.class.filter_backends || []
232
+ return self.filter_backends || []
246
233
  end
247
234
 
248
235
  # Filter an arbitrary data set over all configured filter backends.
@@ -299,7 +286,7 @@ module RESTFramework::BaseControllerMixin
299
286
  end
300
287
 
301
288
  # Do not use any adapters by default, if configured.
302
- if self.class.disable_adapters_by_default && !kwargs.key?(:adapter)
289
+ if self.disable_adapters_by_default && !kwargs.key?(:adapter)
303
290
  kwargs[:adapter] = nil
304
291
  end
305
292
 
@@ -309,27 +296,27 @@ module RESTFramework::BaseControllerMixin
309
296
  begin
310
297
  respond_to do |format|
311
298
  if payload == ""
312
- format.json { head(kwargs[:status] || :no_content) } if self.class.serialize_to_json
313
- format.xml { head(kwargs[:status] || :no_content) } if self.class.serialize_to_xml
299
+ format.json { head(kwargs[:status] || :no_content) } if self.serialize_to_json
300
+ format.xml { head(kwargs[:status] || :no_content) } if self.serialize_to_xml
314
301
  else
315
302
  format.json {
316
303
  jkwargs = kwargs.merge(json_kwargs)
317
304
  render(json: payload, layout: false, **jkwargs)
318
- } if self.class.serialize_to_json
305
+ } if self.serialize_to_json
319
306
  format.xml {
320
307
  xkwargs = kwargs.merge(xml_kwargs)
321
308
  render(xml: payload, layout: false, **xkwargs)
322
- } if self.class.serialize_to_xml
309
+ } if self.serialize_to_xml
323
310
  # TODO: possibly support more formats here if supported?
324
311
  end
325
312
  format.html {
326
313
  @payload = payload
327
314
  if payload == ""
328
- @json_payload = "" if self.class.serialize_to_json
329
- @xml_payload = "" if self.class.serialize_to_xml
315
+ @json_payload = "" if self.serialize_to_json
316
+ @xml_payload = "" if self.serialize_to_xml
330
317
  else
331
- @json_payload = payload.to_json if self.class.serialize_to_json
332
- @xml_payload = payload.to_xml if self.class.serialize_to_xml
318
+ @json_payload = payload.to_json if self.serialize_to_json
319
+ @xml_payload = payload.to_xml if self.serialize_to_xml
333
320
  end
334
321
  @title ||= self.class.get_title
335
322
  @description ||= self.class.description
@@ -347,7 +334,7 @@ module RESTFramework::BaseControllerMixin
347
334
  }
348
335
  end
349
336
  rescue ActionController::UnknownFormat
350
- if !already_rescued_unknown_format && rescue_format = self.class.rescue_unknown_format_with
337
+ if !already_rescued_unknown_format && rescue_format = self.rescue_unknown_format_with
351
338
  request.format = rescue_format
352
339
  already_rescued_unknown_format = true
353
340
  retry
@@ -14,7 +14,7 @@ module RESTFramework::BaseModelControllerMixin
14
14
  }
15
15
  include RESTFramework::BaseControllerMixin
16
16
 
17
- RRF_BASE_MODEL_CONTROLLER_CONFIG = {
17
+ RRF_BASE_MODEL_CONFIG = {
18
18
  # Core attributes related to models.
19
19
  model: nil,
20
20
  recordset: nil,
@@ -22,20 +22,31 @@ module RESTFramework::BaseModelControllerMixin
22
22
  # Attributes for configuring record fields.
23
23
  fields: nil,
24
24
  field_config: nil,
25
- action_fields: nil,
26
25
 
27
26
  # Options for what should be included/excluded from default fields.
28
27
  exclude_associations: false,
29
28
  include_active_storage: false,
30
29
  include_action_text: false,
31
-
30
+ }
31
+ RRF_BASE_MODEL_INSTANCE_CONFIG = {
32
32
  # Attributes for finding records.
33
33
  find_by_fields: nil,
34
34
  find_by_query_param: "find_by",
35
35
 
36
- # Attributes for create/update parameters.
36
+ # Options for handling request body parameters.
37
37
  allowed_parameters: nil,
38
- allowed_action_parameters: nil,
38
+ filter_pk_from_request_body: true,
39
+ exclude_body_fields: %w[
40
+ created_at
41
+ created_by
42
+ created_by_id
43
+ updated_at
44
+ updated_by
45
+ updated_by_id
46
+ _method
47
+ utf8
48
+ authenticity_token
49
+ ].freeze,
39
50
 
40
51
  # Attributes for the default native serializer.
41
52
  native_serializer_config: nil,
@@ -59,13 +70,18 @@ module RESTFramework::BaseModelControllerMixin
59
70
  # Options for association assignment.
60
71
  permit_id_assignment: true,
61
72
  permit_nested_attributes_assignment: true,
62
- allow_all_nested_attributes: false,
63
73
 
64
74
  # Option for `recordset.create` vs `Model.create` behavior.
65
75
  create_from_recordset: true,
66
76
 
67
77
  # Control if filtering is done before find.
68
78
  filter_recordset_before_find: true,
79
+
80
+ # Options for `ransack` filtering.
81
+ ransack_options: nil,
82
+ ransack_query_param: "q",
83
+ ransack_distinct: true,
84
+ ransack_distinct_query_param: "distinct",
69
85
  }
70
86
 
71
87
  module ClassMethods
@@ -130,6 +146,7 @@ module RESTFramework::BaseModelControllerMixin
130
146
  columns = model.columns_hash
131
147
  end
132
148
  config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
149
+ config[:sub_fields] = config[:sub_fields].map(&:to_s)
133
150
 
134
151
  # Serialize very basic metadata about sub-fields.
135
152
  config[:sub_fields_metadata] = config[:sub_fields].map { |sf|
@@ -228,21 +245,17 @@ module RESTFramework::BaseModelControllerMixin
228
245
  if ref = reflections[f]
229
246
  metadata[:kind] = "association"
230
247
 
231
- # Determine if we render id/ids fields.
232
- if self.permit_id_assignment
233
- if ref.collection?
234
- metadata[:id_field] = "#{f.singularize}_ids"
235
- elsif ref.belongs_to?
236
- metadata[:id_field] = "#{f}_id"
237
- end
248
+ # Determine if we render id/ids fields. Unfortunately, `has_one` does not provide this
249
+ # interface.
250
+ if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
251
+ metadata[:id_field] = id_field
238
252
  end
239
253
 
240
254
  # Determine if we render nested attributes options.
241
- if self.permit_nested_attributes_assignment
242
- if nested_opts = model.nested_attributes_options[f.to_sym].presence
243
- nested_opts[:field] = "#{f}_attributes"
244
- metadata[:nested_attributes_options] = nested_opts
245
- end
255
+ if self.permit_nested_attributes_assignment && (
256
+ nested_opts = model.nested_attributes_options[f.to_sym].presence
257
+ )
258
+ metadata[:nested_attributes_options] = {field: "#{f}_attributes", **nested_opts}
246
259
  end
247
260
 
248
261
  begin
@@ -364,7 +377,7 @@ module RESTFramework::BaseModelControllerMixin
364
377
  # self.setup_channel
365
378
 
366
379
  if RESTFramework.config.freeze_config
367
- self::RRF_BASE_MODEL_CONTROLLER_CONFIG.keys.each { |k|
380
+ (self::RRF_BASE_MODEL_CONFIG.keys + self::RRF_BASE_MODEL_INSTANCE_CONFIG.keys).each { |k|
368
381
  v = self.send(k)
369
382
  v.freeze if v.is_a?(Hash) || v.is_a?(Array)
370
383
  }
@@ -381,31 +394,21 @@ module RESTFramework::BaseModelControllerMixin
381
394
  base.extend(ClassMethods)
382
395
 
383
396
  # Add class attributes (with defaults) unless they already exist.
384
- RRF_BASE_MODEL_CONTROLLER_CONFIG.each do |a, default|
397
+ RRF_BASE_MODEL_CONFIG.each do |a, default|
385
398
  next if base.respond_to?(a)
386
399
 
387
- base.class_attribute(a)
388
-
389
- # Set default manually so we can still support Rails 4. Maybe later we can use the default
390
- # parameter on `class_attribute`.
391
- base.send(:"#{a}=", default)
400
+ base.class_attribute(a, default: default, instance_accessor: false)
392
401
  end
393
- end
394
-
395
- def _get_specific_action_config(action_config_key, generic_config_key)
396
- action_config = self.class.send(action_config_key)&.with_indifferent_access || {}
397
- action = self.action_name&.to_sym
398
-
399
- # Index action should use :list serializer if :index is not provided.
400
- action = :list if action == :index && !action_config.key?(:index)
402
+ RRF_BASE_MODEL_INSTANCE_CONFIG.each do |a, default|
403
+ next if base.respond_to?(a)
401
404
 
402
- return (action_config[action] if action) || self.class.send(generic_config_key)
405
+ base.class_attribute(a, default: default)
406
+ end
403
407
  end
404
408
 
405
- # Get a list of fields, taking into account the current action.
409
+ # Get a list of fields for this controller.
406
410
  def get_fields
407
- fields = self._get_specific_action_config(:action_fields, :fields)
408
- return self.class.get_fields(input_fields: fields)
411
+ return self.class.get_fields(input_fields: self.class.fields)
409
412
  end
410
413
 
411
414
  # Pass fields to get dynamic metadata based on which fields are available.
@@ -413,19 +416,11 @@ module RESTFramework::BaseModelControllerMixin
413
416
  return self.class.get_options_metadata
414
417
  end
415
418
 
416
- # Get a list of find_by fields for the current action.
417
- def get_find_by_fields
418
- return self.class.find_by_fields
419
- end
420
-
421
419
  # Get a list of parameters allowed for the current action.
422
420
  def get_allowed_parameters
423
421
  return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
424
422
 
425
- @_get_allowed_parameters = self._get_specific_action_config(
426
- :allowed_action_parameters,
427
- :allowed_parameters,
428
- )
423
+ @_get_allowed_parameters = self.allowed_parameters
429
424
  return @_get_allowed_parameters if @_get_allowed_parameters
430
425
 
431
426
  # For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
@@ -454,20 +449,16 @@ module RESTFramework::BaseModelControllerMixin
454
449
  # Return field if it's not an association.
455
450
  next f unless ref = reflections[f]
456
451
 
457
- if self.class.permit_id_assignment
458
- if ref.collection?
459
- hash_variations["#{f.singularize}_ids"] = []
460
- elsif ref.belongs_to?
461
- variations << "#{f}_id"
452
+ if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
453
+ if id_field.ends_with?("_ids")
454
+ hash_variations[id_field] = []
455
+ else
456
+ variations << id_field
462
457
  end
463
458
  end
464
459
 
465
- if self.class.permit_nested_attributes_assignment
466
- if self.class.allow_all_nested_attributes
467
- hash_variations["#{f}_attributes"] = {}
468
- else
469
- hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
470
- end
460
+ if self.permit_nested_attributes_assignment
461
+ hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
471
462
  end
472
463
 
473
464
  # Associations are not allowed to be submitted in their bare form.
@@ -483,37 +474,57 @@ module RESTFramework::BaseModelControllerMixin
483
474
  return super || RESTFramework::NativeSerializer
484
475
  end
485
476
 
486
- # Get filtering backends, defaulting to using `ModelFilter`, `ModelOrderingFilter`, and
477
+ # Get filtering backends, defaulting to using `ModelQueryFilter`, `ModelOrderingFilter`, and
487
478
  # `ModelSearchFilter`.
488
479
  def get_filter_backends
489
- return self.class.filter_backends || [
490
- RESTFramework::ModelFilter,
480
+ return self.filter_backends || [
481
+ RESTFramework::ModelQueryFilter,
491
482
  RESTFramework::ModelOrderingFilter,
492
483
  RESTFramework::ModelSearchFilter,
493
484
  ]
494
485
  end
495
486
 
496
487
  # Use strong parameters to filter the request body using the configured allowed parameters.
497
- def get_body_params(data: nil, bulk_mode: nil)
498
- data ||= self.request.request_parameters
488
+ def get_body_params(bulk_mode: nil)
489
+ data = self.request.request_parameters
499
490
  pk = self.class.get_model&.primary_key
491
+ allowed_params = self.get_allowed_parameters
492
+
493
+ # Before we filter the data, dynamically dispatch association assignment to either the id/ids
494
+ # assignment ActiveRecord API or the nested assignment ActiveRecord API. Note that there is no
495
+ # need to check for `permit_id_assignment` or `permit_nested_attributes_assignment` here, since
496
+ # that is enforced by strong parameters generated by `get_allowed_parameters`.
497
+ self.class.get_model.reflections.each do |name, ref|
498
+ if payload = data[name]
499
+ if payload.is_a?(Hash) || (payload.is_a?(Array) && payload.all? { |x| x.is_a?(Hash) })
500
+ # Assume nested attributes assignment.
501
+ attributes_key = "#{name}_attributes"
502
+ data[attributes_key] = data.delete(name) unless data[attributes_key]
503
+ elsif id_field = RESTFramework::Utils.get_id_field(name, ref)
504
+ # Assume id/ids assignment.
505
+ data[id_field] = data.delete(name) unless data[id_field]
506
+ end
507
+ end
508
+ end
500
509
 
501
510
  # Filter the request body with strong params. If `bulk` is true, then we apply allowed
502
511
  # parameters to the `_json` key of the request body.
503
- body_params = if bulk_mode
512
+ body_params = if allowed_params == true
513
+ ActionController::Parameters.new(data).permit!
514
+ elsif bulk_mode
504
515
  pk = bulk_mode == :update ? [pk] : []
505
- ActionController::Parameters.new(data).permit({_json: self.get_allowed_parameters + pk})
516
+ ActionController::Parameters.new(data).permit({_json: allowed_params + pk})
506
517
  else
507
- ActionController::Parameters.new(data).permit(*self.get_allowed_parameters)
518
+ ActionController::Parameters.new(data).permit(*allowed_params)
508
519
  end
509
520
 
510
521
  # Filter primary key if configured.
511
- if self.class.filter_pk_from_request_body && bulk_mode != :update
522
+ if self.filter_pk_from_request_body && bulk_mode != :update
512
523
  body_params.delete(pk)
513
524
  end
514
525
 
515
526
  # Filter fields in `exclude_body_fields`.
516
- (self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
527
+ (self.exclude_body_fields || []).each { |f| body_params.delete(f) }
517
528
 
518
529
  # ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
519
530
  #
@@ -595,9 +606,9 @@ module RESTFramework::BaseModelControllerMixin
595
606
  is_pk = true
596
607
 
597
608
  # Find by another column if it's permitted.
598
- if find_by_param = self.class.find_by_query_param.presence
609
+ if find_by_param = self.find_by_query_param.presence
599
610
  if find_by = params[find_by_param].presence
600
- find_by_fields = self.get_find_by_fields&.map(&:to_s)
611
+ find_by_fields = self.find_by_fields&.map(&:to_s)
601
612
 
602
613
  if !find_by_fields || find_by.in?(find_by_fields)
603
614
  is_pk = false unless find_by_key == find_by
@@ -621,7 +632,7 @@ module RESTFramework::BaseModelControllerMixin
621
632
 
622
633
  # Determine what collection to call `create` on.
623
634
  def get_create_from
624
- if self.class.create_from_recordset
635
+ if self.create_from_recordset
625
636
  # Create with any properties inherited from the recordset. We exclude any `select` clauses
626
637
  # in case model callbacks need to call `count` on this collection, which typically raises a
627
638
  # SQL `SyntaxError`.
@@ -660,13 +671,13 @@ module RESTFramework::ListModelMixin
660
671
  records = self.get_records
661
672
 
662
673
  # Handle pagination, if enabled.
663
- if self.class.paginator_class
674
+ if self.paginator_class
664
675
  # If there is no `max_page_size`, `page_size_query_param` is not `nil`, and the page size is
665
676
  # set to "0", then skip pagination.
666
- unless !self.class.max_page_size &&
667
- self.class.page_size_query_param &&
668
- params[self.class.page_size_query_param] == "0"
669
- paginator = self.class.paginator_class.new(data: records, controller: self)
677
+ unless !self.max_page_size &&
678
+ self.page_size_query_param &&
679
+ params[self.page_size_query_param] == "0"
680
+ paginator = self.paginator_class.new(data: records, controller: self)
670
681
  page = paginator.get_page
671
682
  serialized_page = self.serialize(page)
672
683
  return paginator.get_paginated_response(serialized_page)
@@ -2,5 +2,6 @@ module RESTFramework::ControllerMixins
2
2
  end
3
3
 
4
4
  require_relative "controller_mixins/base"
5
+
5
6
  require_relative "controller_mixins/bulk"
6
7
  require_relative "controller_mixins/models"
@@ -1,2 +1,11 @@
1
1
  class RESTFramework::Engine < Rails::Engine
2
+ initializer "rest_framework.assets" do
3
+ config.after_initialize do |app|
4
+ if RESTFramework.config.use_vendored_assets
5
+ app.config.assets.precompile += RESTFramework::EXTERNAL_ASSETS.keys.map do |name|
6
+ "rest_framework/#{name}"
7
+ end
8
+ end
9
+ end
10
+ end
2
11
  end
@@ -0,0 +1,9 @@
1
+ class RESTFramework::BaseFilter
2
+ def initialize(controller:)
3
+ @controller = controller
4
+ end
5
+
6
+ def get_filtered_data(data)
7
+ raise NotImplementedError
8
+ end
9
+ end