rest_framework 0.9.16 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.