rest_framework 0.9.16 → 0.11.0

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.
@@ -24,14 +24,13 @@ module RESTFramework::Mixins::BaseModelControllerMixin
24
24
  fields: nil,
25
25
  field_config: nil,
26
26
 
27
- # Options for what should be included/excluded from default fields.
28
- exclude_associations: false,
29
- }
30
- RRF_BASE_MODEL_INSTANCE_CONFIG = {
31
27
  # Attributes for finding records.
32
28
  find_by_fields: nil,
33
29
  find_by_query_param: "find_by",
34
30
 
31
+ # Options for what should be included/excluded from default fields.
32
+ exclude_associations: false,
33
+
35
34
  # Options for handling request body parameters.
36
35
  allowed_parameters: nil,
37
36
  filter_pk_from_request_body: true,
@@ -103,13 +102,20 @@ module RESTFramework::Mixins::BaseModelControllerMixin
103
102
  # If fields is a hash, then parse it.
104
103
  if input_fields.is_a?(Hash)
105
104
  return RESTFramework::Utils.parse_fields_hash(
106
- input_fields, self.get_model, exclude_associations: self.exclude_associations
105
+ input_fields,
106
+ self.get_model,
107
+ exclude_associations: self.exclude_associations,
108
+ action_text: self.enable_action_text,
109
+ active_storage: self.enable_active_storage,
107
110
  )
108
111
  elsif !input_fields
109
112
  # Otherwise, if fields is nil, then fallback to columns.
110
113
  model = self.get_model
111
114
  return model ? RESTFramework::Utils.fields_for(
112
- model, exclude_associations: self.exclude_associations
115
+ model,
116
+ exclude_associations: self.exclude_associations,
117
+ action_text: self.enable_action_text,
118
+ active_storage: self.enable_active_storage,
113
119
  ) : []
114
120
  elsif input_fields
115
121
  input_fields = input_fields.map(&:to_s)
@@ -118,113 +124,72 @@ module RESTFramework::Mixins::BaseModelControllerMixin
118
124
  return input_fields
119
125
  end
120
126
 
121
- # Get a field's config, including defaults.
122
- def field_config_for(f)
123
- f = f.to_sym
124
- @_field_config_for ||= {}
125
- return @_field_config_for[f] if @_field_config_for[f]
126
-
127
- config = self.field_config&.dig(f) || {}
128
-
129
- # Default sub-fields if field is an association.
130
- if ref = self.get_model.reflections[f.to_s]
131
- if ref.polymorphic?
132
- columns = {}
133
- else
134
- model = ref.klass
135
- columns = model.columns_hash
136
- end
137
- config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
138
- config[:sub_fields] = config[:sub_fields].map(&:to_s)
139
-
140
- # Serialize very basic metadata about sub-fields.
141
- config[:sub_fields_metadata] = config[:sub_fields].map { |sf|
142
- v = {}
143
-
144
- if columns[sf]
145
- v[:kind] = "column"
146
- end
147
-
148
- next [sf, v]
149
- }.to_h.compact.presence
150
- end
151
-
152
- return @_field_config_for[f] = config.compact
153
- end
154
-
155
- # Get metadata about the resource's fields.
156
- def fields_metadata
157
- return @_fields_metadata if @_fields_metadata
127
+ # Get a full field configuration, including defaults and inferred values.
128
+ def field_configuration
129
+ return @field_configuration if @field_configuration
158
130
 
159
- # Get metadata sources.
131
+ field_config = self.field_config&.with_indifferent_access || {}
160
132
  model = self.get_model
161
- fields = self.get_fields.map(&:to_s)
162
133
  columns = model.columns_hash
163
134
  column_defaults = model.column_defaults
164
135
  reflections = model.reflections
165
136
  attributes = model._default_attributes
166
137
  readonly_attributes = model.readonly_attributes
167
- exclude_body_fields = self.exclude_body_fields.map(&:to_s)
138
+ exclude_body_fields = self.exclude_body_fields&.map(&:to_s)
168
139
  rich_text_association_names = model.reflect_on_all_associations(:has_one)
169
140
  .collect(&:name)
170
141
  .select { |n| n.to_s.start_with?("rich_text_") }
171
142
  attachment_reflections = model.attachment_reflections
172
143
 
173
- return @_fields_metadata = fields.map { |f|
174
- # Initialize metadata to make the order consistent.
175
- metadata = {
176
- type: nil,
177
- kind: nil,
178
- label: self.label_for(f),
179
- primary_key: nil,
180
- required: nil,
181
- read_only: nil,
182
- }
144
+ return @field_configuration = self.get_fields.map { |f|
145
+ cfg = field_config[f]&.dup || {}
146
+ cfg[:label] ||= self.label_for(f)
183
147
 
184
- # Determine `primary_key` based on model.
148
+ # Annotate primary key.
185
149
  if model.primary_key == f
186
- metadata[:primary_key] = true
150
+ cfg[:primary_key] = true
151
+
152
+ unless cfg.key?(:readonly)
153
+ cfg[:readonly] = true
154
+ end
187
155
  end
188
156
 
189
- # Determine if the field is a read-only attribute.
190
- if metadata[:primary_key] || f.in?(readonly_attributes) || f.in?(exclude_body_fields)
191
- metadata[:read_only] = true
157
+ # Annotate readonly attributes.
158
+ if f.in?(readonly_attributes) || f.in?(exclude_body_fields)
159
+ cfg[:readonly] = true
192
160
  end
193
161
 
194
- # Determine `type`, `required`, `label`, and `kind` based on schema.
162
+ # Annotate column data.
195
163
  if column = columns[f]
196
- metadata[:kind] = "column"
197
- metadata[:type] = column.type
198
- metadata[:required] = true unless column.null
164
+ cfg[:kind] = "column"
165
+ cfg[:type] ||= column.type
166
+ cfg[:required] = true unless column.null
199
167
  end
200
168
 
201
- # Determine `default` based on schema; we use `column_defaults` rather than `columns_hash`
202
- # because these are casted to the proper type.
203
- column_default = column_defaults[f]
204
- unless column_default.nil?
205
- metadata[:default] = column_default
169
+ # Add default values from the model's schema.
170
+ if column_default = column_defaults[f] && !cfg[:default].nil?
171
+ cfg[:default] ||= column_default
206
172
  end
207
173
 
208
- # Extract details from the model's attributes hash.
209
- if attributes.key?(f) && attribute = attributes[f]
210
- unless metadata.key?(:default)
211
- default = attribute.value_before_type_cast
212
- metadata[:default] = default unless default.nil?
174
+ # Add metadata from the model's attributes hash.
175
+ if attribute = attributes[f]
176
+ if cfg[:default].nil? && default = attribute.value_before_type_cast
177
+ cfg[:default] = default
213
178
  end
214
- metadata[:kind] ||= "attribute"
179
+ cfg[:kind] ||= "attribute"
215
180
 
216
181
  # Get any type information from the attribute.
217
182
  if type = attribute.type
218
- metadata[:type] ||= type.type
183
+ cfg[:type] ||= type.type if type.type
219
184
 
220
185
  # Get enum variants.
221
186
  if type.is_a?(ActiveRecord::Enum::EnumType)
222
- metadata[:enum_variants] = type.send(:mapping)
187
+ cfg[:enum_variants] = type.send(:mapping)
223
188
 
224
- # Custom integration with `translate_enum`.
189
+ # TranslateEnum Integration:
225
190
  translate_method = "translated_#{f.pluralize}"
226
191
  if model.respond_to?(translate_method)
227
- metadata[:enum_translations] = model.send(translate_method)
192
+ cfg[:enum_translations] = model.send(translate_method)
228
193
  end
229
194
  end
230
195
  end
@@ -232,53 +197,65 @@ module RESTFramework::Mixins::BaseModelControllerMixin
232
197
 
233
198
  # Get association metadata.
234
199
  if ref = reflections[f]
235
- metadata[:kind] = "association"
200
+ cfg[:kind] = "association"
201
+
202
+ # Determine sub-fields for associations.
203
+ if ref.polymorphic?
204
+ ref_columns = {}
205
+ else
206
+ ref_columns = ref.klass.columns_hash
207
+ end
208
+ cfg[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
209
+ cfg[:sub_fields] = cfg[:sub_fields].map(&:to_s)
210
+
211
+ # Very basic metadata about sub-fields.
212
+ cfg[:sub_fields_metadata] = cfg[:sub_fields].map { |sf|
213
+ v = {}
214
+
215
+ if ref_columns[sf]
216
+ v[:kind] = "column"
217
+ else
218
+ v[:kind] = "method"
219
+ end
220
+
221
+ next [sf, v]
222
+ }.to_h.compact.presence
236
223
 
237
224
  # Determine if we render id/ids fields. Unfortunately, `has_one` does not provide this
238
225
  # interface.
239
- if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
240
- metadata[:id_field] = id_field
226
+ if self.permit_id_assignment && id_field = RESTFramework::Utils.id_field_for(f, ref)
227
+ cfg[:id_field] = id_field
241
228
  end
242
229
 
243
230
  # Determine if we render nested attributes options.
244
231
  if self.permit_nested_attributes_assignment && (
245
232
  nested_opts = model.nested_attributes_options[f.to_sym].presence
246
233
  )
247
- metadata[:nested_attributes_options] = {field: "#{f}_attributes", **nested_opts}
234
+ cfg[:nested_attributes_options] = {field: "#{f}_attributes", **nested_opts}
248
235
  end
249
236
 
250
237
  begin
251
- pk = ref.active_record_primary_key
238
+ cfg[:association_pk] = ref.active_record_primary_key
252
239
  rescue ActiveRecord::UnknownPrimaryKey
253
240
  end
254
- metadata[:association] = {
255
- macro: ref.macro,
256
- collection: ref.collection?,
257
- class_name: ref.class_name,
258
- foreign_key: ref.foreign_key,
259
- primary_key: pk,
260
- polymorphic: ref.polymorphic?,
261
- table_name: ref.polymorphic? ? nil : ref.table_name,
262
- options: ref.options.as_json.presence,
263
- }.compact
241
+
242
+ cfg[:reflection] = ref
264
243
  end
265
244
 
266
245
  # Determine if this is an ActionText "rich text".
267
246
  if :"rich_text_#{f}".in?(rich_text_association_names)
268
- metadata[:kind] = "rich_text"
247
+ cfg[:kind] = "rich_text"
269
248
  end
270
249
 
271
250
  # Determine if this is an ActiveStorage attachment.
272
251
  if ref = attachment_reflections[f]
273
- metadata[:kind] = "attachment"
274
- metadata[:attachment] = {
275
- macro: ref.macro,
276
- }
252
+ cfg[:kind] = "attachment"
253
+ cfg[:attachment_type] = ref.macro
277
254
  end
278
255
 
279
256
  # Determine if this is just a method.
280
- if !metadata[:kind] && model.method_defined?(f)
281
- metadata[:kind] = "method"
257
+ if !cfg[:kind] && model.method_defined?(f)
258
+ cfg[:kind] = "method"
282
259
  end
283
260
 
284
261
  # Collect validator options into a hash on their type, while also updating `required` based
@@ -292,7 +269,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
292
269
  next if IGNORE_VALIDATORS_WITH_KEYS.any? { |k| options.key?(k) }
293
270
 
294
271
  # Update `required` if we find a presence validator.
295
- metadata[:required] = true if kind == :presence
272
+ cfg[:required] = true if kind == :presence
296
273
 
297
274
  # Resolve procs (and lambdas), and symbols for certain arguments.
298
275
  if options[:in].is_a?(Proc)
@@ -301,27 +278,65 @@ module RESTFramework::Mixins::BaseModelControllerMixin
301
278
  options = options.merge(in: model.send(options[:in]))
302
279
  end
303
280
 
304
- metadata[:validators] ||= {}
305
- metadata[:validators][kind] ||= []
306
- metadata[:validators][kind] << options
281
+ cfg[:validators] ||= {}
282
+ cfg[:validators][kind] ||= []
283
+ cfg[:validators][kind] << options
307
284
  end
308
285
 
309
- # Serialize any field config.
310
- metadata[:config] = self.field_config_for(f).presence
311
-
312
- next [f, metadata.compact]
313
- }.to_h
286
+ next [f, cfg]
287
+ }.to_h.with_indifferent_access
314
288
  end
315
289
 
316
- # Get a hash of metadata to be rendered in the `OPTIONS` response.
317
- def options_metadata
318
- return super.merge(
319
- {
320
- primary_key: self.get_model.primary_key,
321
- fields: self.fields_metadata,
322
- callbacks: self._process_action_callbacks.as_json,
323
- },
324
- )
290
+ def openapi_schema
291
+ return @openapi_schema if @openapi_schema
292
+
293
+ field_configuration = self.field_configuration
294
+ @openapi_schema = {
295
+ required: field_configuration.select { |_, cfg| cfg[:required] }.keys,
296
+ type: "object",
297
+ properties: field_configuration.map { |f, cfg|
298
+ v = {title: cfg[:label]}
299
+
300
+ if cfg[:kind] == "association"
301
+ v[:type] = cfg[:reflection].collection? ? "array" : "object"
302
+ elsif cfg[:kind] == "rich_text"
303
+ v[:type] = "string"
304
+ v[:"x-rrf-rich_text"] = true
305
+ elsif cfg[:kind] == "attachment"
306
+ v[:type] = "string"
307
+ v[:"x-rrf-attachment"] = cfg[:attachment_type]
308
+ else
309
+ v[:type] = cfg[:type]
310
+ end
311
+
312
+ v[:readOnly] = true if cfg[:readonly]
313
+ v[:default] = cfg[:default] if cfg.key?(:default)
314
+
315
+ if enum_variants = cfg[:enum_variants]
316
+ v[:enum] = enum_variants.keys
317
+ v[:"x-rrf-enum_variants"] = enum_variants
318
+ end
319
+
320
+ if validators = cfg[:validators]
321
+ v[:"x-rrf-validators"] = validators
322
+ end
323
+
324
+ v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind]
325
+
326
+ if cfg[:reflection]
327
+ v[:"x-rrf-reflection"] = cfg[:reflection]
328
+ v[:"x-rrf-association_pk"] = cfg[:association_pk]
329
+ v[:"x-rrf-sub_fields"] = cfg[:sub_fields]
330
+ v[:"x-rrf-sub_fields_metadata"] = cfg[:sub_fields_metadata]
331
+ v[:"x-rrf-id_field"] = cfg[:id_field]
332
+ v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
333
+ end
334
+
335
+ next [f, v]
336
+ }.to_h,
337
+ }
338
+
339
+ return @openapi_schema
325
340
  end
326
341
 
327
342
  def setup_delegation
@@ -334,9 +349,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
334
349
  model = self.class.get_model
335
350
 
336
351
  if model.method(action).parameters.last&.first == :keyrest
337
- return api_response(model.send(action, **params))
352
+ return render_api(model.send(action, **params))
338
353
  else
339
- return api_response(model.send(action))
354
+ return render_api(model.send(action))
340
355
  end
341
356
  end
342
357
  end
@@ -350,9 +365,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
350
365
  record = self.get_record
351
366
 
352
367
  if record.method(action).parameters.last&.first == :keyrest
353
- return api_response(record.send(action, **params))
368
+ return render_api(record.send(action, **params))
354
369
  else
355
- return api_response(record.send(action))
370
+ return render_api(record.send(action))
356
371
  end
357
372
  end
358
373
  end
@@ -366,7 +381,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
366
381
  # self.setup_channel
367
382
 
368
383
  if RESTFramework.config.freeze_config
369
- (self::RRF_BASE_MODEL_CONFIG.keys + self::RRF_BASE_MODEL_INSTANCE_CONFIG.keys).each { |k|
384
+ self::RRF_BASE_MODEL_CONFIG.keys.each { |k|
370
385
  v = self.send(k)
371
386
  v.freeze if v.is_a?(Hash) || v.is_a?(Array)
372
387
  }
@@ -388,27 +403,51 @@ module RESTFramework::Mixins::BaseModelControllerMixin
388
403
 
389
404
  base.class_attribute(a, default: default, instance_accessor: false)
390
405
  end
391
- RRF_BASE_MODEL_INSTANCE_CONFIG.each do |a, default|
392
- next if base.respond_to?(a)
393
-
394
- base.class_attribute(a, default: default)
395
- end
396
406
  end
397
407
 
398
- # Get a list of fields for this controller.
399
408
  def get_fields
400
409
  return self.class.get_fields(input_fields: self.class.fields)
401
410
  end
402
411
 
403
- def options_metadata
404
- return self.class.options_metadata
412
+ def openapi_metadata
413
+ data = super
414
+ routes = self.route_groups.values[0]
415
+ schema_name = routes[0][:controller].camelize.gsub("::", ".")
416
+
417
+ # Insert schema into metadata.
418
+ data[:components] ||= {}
419
+ data[:components][:schemas] ||= {}
420
+ data[:components][:schemas][schema_name] = self.class.openapi_schema
421
+
422
+ # Reference schema for specific actions with a `requestBody`.
423
+ data[:paths].each do |_path, actions|
424
+ actions.each do |_method, action|
425
+ next unless action.is_a?(Hash)
426
+
427
+ injectables = [action.dig(:requestBody, :content), *action[:responses].values.map { |r|
428
+ r[:content]
429
+ }].compact
430
+ injectables.each do |i|
431
+ i.each do |_, v|
432
+ v[:schema] = {"$ref" => "#/components/schemas/#{schema_name}"}
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ return data.merge(
439
+ {
440
+ "x-rrf-primary_key" => self.class.get_model.primary_key,
441
+ "x-rrf-callbacks" => self._process_action_callbacks.as_json,
442
+ },
443
+ )
405
444
  end
406
445
 
407
- # Get a list of parameters allowed for the current action.
446
+ # Get a hash of strong parameters for the current action.
408
447
  def get_allowed_parameters
409
448
  return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
410
449
 
411
- @_get_allowed_parameters = self.allowed_parameters
450
+ @_get_allowed_parameters = self.class.allowed_parameters
412
451
  return @_get_allowed_parameters if @_get_allowed_parameters
413
452
 
414
453
  # Assemble strong parameters.
@@ -417,46 +456,49 @@ module RESTFramework::Mixins::BaseModelControllerMixin
417
456
  reflections = self.class.get_model.reflections
418
457
  @_get_allowed_parameters = self.get_fields.map { |f|
419
458
  f = f.to_s
459
+ config = self.class.field_configuration[f]
420
460
 
421
- # ActiveStorage Integration: `has_one_attached`.
422
- if reflections.key?("#{f}_attachment")
423
- hash_variations[f] = ACTIVESTORAGE_KEYS
461
+ # ActionText Integration:
462
+ if self.class.enable_action_text && reflections.key?("rich_test_#{f}")
424
463
  next f
425
464
  end
426
465
 
427
- # ActiveStorage Integration: `has_many_attached`.
428
- if reflections.key?("#{f}_attachments")
466
+ # ActiveStorage Integration: `has_one_attached`
467
+ if self.class.enable_active_storage && reflections.key?("#{f}_attachment")
429
468
  hash_variations[f] = ACTIVESTORAGE_KEYS
430
- next nil
469
+ next f
431
470
  end
432
471
 
433
- # ActionText Integration.
434
- if reflections.key?("rich_test_#{f}")
435
- next f
472
+ # ActiveStorage Integration: `has_many_attached`
473
+ if self.class.enable_active_storage && reflections.key?("#{f}_attachments")
474
+ hash_variations[f] = ACTIVESTORAGE_KEYS
475
+ next nil
436
476
  end
437
477
 
438
- # Return field if it's not an association.
439
- next f unless ref = reflections[f]
478
+ if config[:reflection]
479
+ # Add `_id`/`_ids` variations for associations.
480
+ if id_field = config[:id_field]
481
+ if id_field.ends_with?("_ids")
482
+ hash_variations[id_field] = []
483
+ else
484
+ variations << id_field
485
+ end
486
+ end
440
487
 
441
- # Add `_id`/`_ids` variations for associations.
442
- if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
443
- if id_field.ends_with?("_ids")
444
- hash_variations[id_field] = []
445
- else
446
- variations << id_field
488
+ # Add `_attributes` variations for associations.
489
+ # TODO: Consider adjusting this based on `nested_attributes_options`.
490
+ if self.class.permit_nested_attributes_assignment
491
+ hash_variations["#{f}_attributes"] = (
492
+ config[:sub_fields] + ["_destroy"]
493
+ )
447
494
  end
448
- end
449
495
 
450
- # Add `_attributes` variations for associations.
451
- if self.permit_nested_attributes_assignment
452
- hash_variations["#{f}_attributes"] = (
453
- self.class.field_config_for(f)[:sub_fields] + ["_destroy"]
454
- )
496
+ # Associations are not allowed to be submitted in their bare form (if they are submitted
497
+ # that way, they will be translated to either id/ids or nested attributes assignment).
498
+ next nil
455
499
  end
456
500
 
457
- # Associations are not allowed to be submitted in their bare form (if they are submitted that
458
- # way, they will be translated to either ID assignment or nested attributes assignment).
459
- next nil
501
+ next f
460
502
  }.compact
461
503
  @_get_allowed_parameters += variations
462
504
  @_get_allowed_parameters << hash_variations
@@ -464,22 +506,11 @@ module RESTFramework::Mixins::BaseModelControllerMixin
464
506
  return @_get_allowed_parameters
465
507
  end
466
508
 
467
- def serializer_class
509
+ def get_serializer_class
468
510
  return super || RESTFramework::NativeSerializer
469
511
  end
470
512
 
471
- def apply_filters(data)
472
- # TODO: Compatibility; remove in 1.0.
473
- if filtered_data = self.try(:get_filtered_data, data)
474
- return filtered_data
475
- end
476
-
477
- return self.filter_backends&.reduce(data) { |d, filter|
478
- filter.new(controller: self).filter_data(d)
479
- } || data
480
- end
481
-
482
- # Use strong parameters to filter the request body using the configured allowed parameters.
513
+ # Use strong parameters to filter the request body.
483
514
  def get_body_params(bulk_mode: nil)
484
515
  data = self.request.request_parameters
485
516
  pk = self.class.get_model&.primary_key
@@ -495,7 +526,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
495
526
  # Assume nested attributes assignment.
496
527
  attributes_key = "#{name}_attributes"
497
528
  data[attributes_key] = data.delete(name) unless data[attributes_key]
498
- elsif id_field = RESTFramework::Utils.get_id_field(name, ref)
529
+ elsif id_field = RESTFramework::Utils.id_field_for(name, ref)
499
530
  # Assume id/ids assignment.
500
531
  data[id_field] = data.delete(name) unless data[id_field]
501
532
  end
@@ -513,31 +544,33 @@ module RESTFramework::Mixins::BaseModelControllerMixin
513
544
  #
514
545
  # rubocop:enable Layout/LineLength
515
546
  has_many_attached_scalar_data = {}
516
- self.class.get_model.attachment_reflections.keys.each do |k|
517
- if data[k].is_a?(Array)
518
- data[k] = data[k].map { |v|
519
- if v.is_a?(String)
520
- v = BASE64_TRANSLATE.call(k, v)
521
-
522
- # Remember scalars because Rails strong params will remove it.
547
+ if self.class.enable_active_storage
548
+ self.class.get_model.attachment_reflections.keys.each do |k|
549
+ if data[k].is_a?(Array)
550
+ data[k] = data[k].map { |v|
523
551
  if v.is_a?(String)
524
- has_many_attached_scalar_data[k] ||= []
525
- has_many_attached_scalar_data[k] << v
526
- end
527
- elsif v.is_a?(Hash)
528
- if v[:io].is_a?(String)
529
- v[:io] = StringIO.new(Base64.decode64(v[:io]))
552
+ v = BASE64_TRANSLATE.call(k, v)
553
+
554
+ # Remember scalars because Rails strong params will remove it.
555
+ if v.is_a?(String)
556
+ has_many_attached_scalar_data[k] ||= []
557
+ has_many_attached_scalar_data[k] << v
558
+ end
559
+ elsif v.is_a?(Hash)
560
+ if v[:io].is_a?(String)
561
+ v[:io] = StringIO.new(Base64.decode64(v[:io]))
562
+ end
530
563
  end
531
- end
532
564
 
533
- next v
534
- }
535
- elsif data[k].is_a?(Hash)
536
- if data[k][:io].is_a?(String)
537
- data[k][:io] = StringIO.new(Base64.decode64(data[k][:io]))
565
+ next v
566
+ }
567
+ elsif data[k].is_a?(Hash)
568
+ if data[k][:io].is_a?(String)
569
+ data[k][:io] = StringIO.new(Base64.decode64(data[k][:io]))
570
+ end
571
+ elsif data[k].is_a?(String)
572
+ data[k] = BASE64_TRANSLATE.call(k, data[k])
538
573
  end
539
- elsif data[k].is_a?(String)
540
- data[k] = BASE64_TRANSLATE.call(k, data[k])
541
574
  end
542
575
  end
543
576
 
@@ -562,12 +595,12 @@ module RESTFramework::Mixins::BaseModelControllerMixin
562
595
  end
563
596
 
564
597
  # Filter primary key, if configured.
565
- if self.filter_pk_from_request_body && bulk_mode != :update
598
+ if self.class.filter_pk_from_request_body && bulk_mode != :update
566
599
  body_params.delete(pk)
567
600
  end
568
601
 
569
602
  # Filter fields in `exclude_body_fields`.
570
- (self.exclude_body_fields || []).each { |f| body_params.delete(f) }
603
+ (self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
571
604
 
572
605
  return body_params
573
606
  end
@@ -586,13 +619,16 @@ module RESTFramework::Mixins::BaseModelControllerMixin
586
619
  return nil
587
620
  end
588
621
 
589
- # Get the records this controller has access to *after* any filtering is applied.
622
+ # Filter the recordset and return records this request has access to.
590
623
  def get_records
591
- return @records ||= self.apply_filters(self.get_recordset)
624
+ data = self.get_recordset
625
+
626
+ return @records ||= self.class.filter_backends&.reduce(data) { |d, filter|
627
+ filter.new(controller: self).filter_data(d)
628
+ } || data
592
629
  end
593
630
 
594
- # Get a single record by primary key or another column, if allowed. The return value is memoized
595
- # and exposed to the view as the `@record` instance variable.
631
+ # Get a single record by primary key or another column, if allowed.
596
632
  def get_record
597
633
  return @record if @record
598
634
 
@@ -600,9 +636,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
600
636
  is_pk = true
601
637
 
602
638
  # Find by another column if it's permitted.
603
- if find_by_param = self.find_by_query_param.presence
639
+ if find_by_param = self.class.find_by_query_param.presence
604
640
  if find_by = params[find_by_param].presence
605
- find_by_fields = self.find_by_fields&.map(&:to_s)
641
+ find_by_fields = self.class.find_by_fields&.map(&:to_s)
606
642
 
607
643
  if !find_by_fields || find_by.in?(find_by_fields)
608
644
  is_pk = false unless find_by_key == find_by
@@ -612,7 +648,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
612
648
  end
613
649
 
614
650
  # Get the recordset, filtering if configured.
615
- collection = if self.filter_recordset_before_find
651
+ collection = if self.class.filter_recordset_before_find
616
652
  self.get_records
617
653
  else
618
654
  self.get_recordset
@@ -628,7 +664,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
628
664
 
629
665
  # Determine what collection to call `create` on.
630
666
  def get_create_from
631
- if self.create_from_recordset
667
+ if self.class.create_from_recordset
632
668
  # Create with any properties inherited from the recordset. We exclude any `select` clauses
633
669
  # in case model callbacks need to call `count` on this collection, which typically raises a
634
670
  # SQL `SyntaxError`.
@@ -645,19 +681,17 @@ module RESTFramework::Mixins::BaseModelControllerMixin
645
681
  # This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
646
682
  # the serializer directly. This would fail for active model serializers, but maybe we don't
647
683
  # care?
648
- s = RESTFramework::Utils.wrap_ams(self.serializer_class)
649
- serialized_records = records.map do |record|
684
+ s = RESTFramework::Utils.wrap_ams(self.get_serializer_class)
685
+ return records.map do |record|
650
686
  s.new(record, controller: self).serialize.merge!({errors: record.errors.presence}.compact)
651
687
  end
652
-
653
- return serialized_records
654
688
  end
655
689
  end
656
690
 
657
691
  # Mixin for listing records.
658
692
  module RESTFramework::Mixins::ListModelMixin
659
693
  def index
660
- return api_response(self.get_index_records)
694
+ return render_api(self.get_index_records)
661
695
  end
662
696
 
663
697
  # Get records with both filtering and pagination applied.
@@ -665,13 +699,13 @@ module RESTFramework::Mixins::ListModelMixin
665
699
  records = self.get_records
666
700
 
667
701
  # Handle pagination, if enabled.
668
- if self.paginator_class
669
- # If there is no `max_page_size`, `page_size_query_param` is not `nil`, and the page size is
670
- # set to "0", then skip pagination.
671
- unless !self.max_page_size &&
672
- self.page_size_query_param &&
673
- params[self.page_size_query_param] == "0"
674
- paginator = self.paginator_class.new(data: records, controller: self)
702
+ if paginator_class = self.class.paginator_class
703
+ # Paginate if there is a `max_page_size`, or if there is no `page_size_query_param`, or if the
704
+ # page size is not set to "0".
705
+ max_page_size = self.class.max_page_size
706
+ page_size_query_param = self.class.page_size_query_param
707
+ if max_page_size || !page_size_query_param || params[page_size_query_param] != "0"
708
+ paginator = paginator_class.new(data: records, controller: self)
675
709
  page = paginator.get_page
676
710
  serialized_page = self.serialize(page)
677
711
  return paginator.get_paginated_response(serialized_page)
@@ -685,14 +719,14 @@ end
685
719
  # Mixin for showing records.
686
720
  module RESTFramework::Mixins::ShowModelMixin
687
721
  def show
688
- return api_response(self.get_record)
722
+ return render_api(self.get_record)
689
723
  end
690
724
  end
691
725
 
692
726
  # Mixin for creating records.
693
727
  module RESTFramework::Mixins::CreateModelMixin
694
728
  def create
695
- return api_response(self.create!, status: :created)
729
+ return render_api(self.create!, status: :created)
696
730
  end
697
731
 
698
732
  # Perform the `create!` call and return the created record.
@@ -704,7 +738,7 @@ end
704
738
  # Mixin for updating records.
705
739
  module RESTFramework::Mixins::UpdateModelMixin
706
740
  def update
707
- return api_response(self.update!)
741
+ return render_api(self.update!)
708
742
  end
709
743
 
710
744
  # Perform the `update!` call and return the updated record.
@@ -719,7 +753,7 @@ end
719
753
  module RESTFramework::Mixins::DestroyModelMixin
720
754
  def destroy
721
755
  self.destroy!
722
- return api_response("")
756
+ return render_api("")
723
757
  end
724
758
 
725
759
  # Perform the `destroy!` call and return the destroyed (and frozen) record.