rest_framework 0.9.2 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/views/layouts/rest_framework.html.erb +6 -1
  4. data/app/views/rest_framework/_head.html.erb +2 -194
  5. data/app/views/rest_framework/head/_external.html.erb +13 -0
  6. data/{docs/assets/js/rest_framework.js → app/views/rest_framework/head/_shared.html} +69 -42
  7. data/docs/Gemfile +1 -0
  8. data/docs/Gemfile.lock +14 -14
  9. data/docs/_config.yml +4 -2
  10. data/docs/_guide/2_controllers.md +342 -0
  11. data/docs/_guide/3_serializers.md +1 -1
  12. data/docs/_guide/4_filtering_and_ordering.md +8 -8
  13. data/docs/_includes/external.html +9 -0
  14. data/docs/_includes/head.html +135 -15
  15. data/docs/_includes/shared.html +164 -0
  16. data/lib/rest_framework/controller_mixins/base.rb +23 -36
  17. data/lib/rest_framework/controller_mixins/models.rb +86 -75
  18. data/lib/rest_framework/controller_mixins.rb +1 -0
  19. data/lib/rest_framework/engine.rb +9 -0
  20. data/lib/rest_framework/filters/base.rb +9 -0
  21. data/lib/rest_framework/filters/model_ordering.rb +48 -0
  22. data/lib/rest_framework/filters/model_query.rb +51 -0
  23. data/lib/rest_framework/filters/model_search.rb +41 -0
  24. data/lib/rest_framework/filters/ransack.rb +25 -0
  25. data/lib/rest_framework/filters.rb +6 -150
  26. data/lib/rest_framework/paginators.rb +7 -11
  27. data/lib/rest_framework/serializers.rb +10 -10
  28. data/lib/rest_framework/utils.rb +15 -7
  29. data/lib/rest_framework.rb +93 -4
  30. data/vendor/assets/javascripts/rest_framework/bootstrap.js +7 -0
  31. data/vendor/assets/javascripts/rest_framework/highlight-json.js +7 -0
  32. data/vendor/assets/javascripts/rest_framework/highlight-xml.js +29 -0
  33. data/vendor/assets/javascripts/rest_framework/highlight.js +1202 -0
  34. data/vendor/assets/javascripts/rest_framework/neatjson.js +8 -0
  35. data/vendor/assets/javascripts/rest_framework/trix.js +6 -0
  36. data/vendor/assets/stylesheets/rest_framework/bootstrap-icons.css +13 -0
  37. data/vendor/assets/stylesheets/rest_framework/bootstrap.css +6 -0
  38. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-dark.css +7 -0
  39. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-light.css +7 -0
  40. data/vendor/assets/stylesheets/rest_framework/trix.css +410 -0
  41. metadata +23 -5
  42. data/docs/_guide/2_controller_mixins.md +0 -293
  43. 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