Class: JSONAPI::Resource

Inherits:
Object
  • Object
show all
Includes:
Callbacks
Defined in:
lib/jsonapi/resource.rb

Constant Summary collapse

@@resource_types =
{}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Callbacks

included

Constructor Details

#initialize(model, context = nil) ⇒ Resource

Returns a new instance of Resource.



24
25
26
27
# File 'lib/jsonapi/resource.rb', line 24

def initialize(model, context = nil)
  @model = model
  @context = context
end

Class Attribute Details

._allowed_filtersObject

Returns the value of attribute _allowed_filters.



261
262
263
# File 'lib/jsonapi/resource.rb', line 261

def _allowed_filters
  @_allowed_filters
end

._associationsObject

Returns the value of attribute _associations.



261
262
263
# File 'lib/jsonapi/resource.rb', line 261

def _associations
  @_associations
end

._attributesObject

Returns the value of attribute _attributes.



261
262
263
# File 'lib/jsonapi/resource.rb', line 261

def _attributes
  @_attributes
end

._paginatorObject

Returns the value of attribute _paginator.



261
262
263
# File 'lib/jsonapi/resource.rb', line 261

def _paginator
  @_paginator
end

._typeObject

Returns the value of attribute _type.



261
262
263
# File 'lib/jsonapi/resource.rb', line 261

def _type
  @_type
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



9
10
11
# File 'lib/jsonapi/resource.rb', line 9

def context
  @context
end

#modelObject (readonly)

Returns the value of attribute model.



10
11
12
# File 'lib/jsonapi/resource.rb', line 10

def model
  @model
end

Class Method Details

._allowed_filter?(filter) ⇒ Boolean

Returns:

  • (Boolean)


576
577
578
# File 'lib/jsonapi/resource.rb', line 576

def _allowed_filter?(filter)
  !_allowed_filters[filter].nil?
end

._as_parent_keyObject



547
548
549
# File 'lib/jsonapi/resource.rb', line 547

def _as_parent_key
  @_as_parent_key ||= "#{_type.to_s.singularize}_#{_primary_key}"
end

._association(type) ⇒ Object



534
535
536
537
# File 'lib/jsonapi/resource.rb', line 534

def _association(type)
  type = type.to_sym
  @_associations[type]
end

._attribute_options(attr) ⇒ Object

quasi private class methods



521
522
523
# File 'lib/jsonapi/resource.rb', line 521

def _attribute_options(attr)
  default_attribute_options.merge(@_attributes[attr])
end

._has_association?(type) ⇒ Boolean

Returns:

  • (Boolean)


529
530
531
532
# File 'lib/jsonapi/resource.rb', line 529

def _has_association?(type)
  type = type.to_s
  @_associations.has_key?(type.singularize.to_sym) || @_associations.has_key?(type.pluralize.to_sym)
end

._model_classObject



572
573
574
# File 'lib/jsonapi/resource.rb', line 572

def _model_class
  @model ||= _model_name.to_s.safe_constantize
end

._model_nameObject



539
540
541
# File 'lib/jsonapi/resource.rb', line 539

def _model_name
  @_model_name ||= self.name.demodulize.sub(/Resource$/, '')
end

._primary_keyObject



543
544
545
# File 'lib/jsonapi/resource.rb', line 543

def _primary_key
  @_primary_key ||= :id
end

._resource_name_from_type(type) ⇒ Object



555
556
557
558
559
560
561
562
# File 'lib/jsonapi/resource.rb', line 555

def _resource_name_from_type(type)
  class_name = @@resource_types[type]
  if class_name.nil?
    class_name = "#{type.to_s.singularize}_resource".camelize
    @@resource_types[type] = class_name
  end
  return class_name
end

._updatable_associationsObject



525
526
527
# File 'lib/jsonapi/resource.rb', line 525

def _updatable_associations
  @_associations.map { |key, association| key }
end

.apply_filter(records, filter, value, options = {}) ⇒ Object



387
388
389
# File 'lib/jsonapi/resource.rb', line 387

def apply_filter(records, filter, value, options = {})
  records.where(filter => value)
end

.apply_filters(records, filters, options = {}) ⇒ Object



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/jsonapi/resource.rb', line 391

def apply_filters(records, filters, options = {})
  required_includes = []

  if filters
    filters.each do |filter, value|
      if _associations.include?(filter)
        if _associations[filter].is_a?(JSONAPI::Association::HasMany)
          required_includes.push(filter)
          records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options)
        else
          records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options)
        end
      else
        records = apply_filter(records, filter, value, options)
      end
    end
  end

  if required_includes.any?
    records.includes(required_includes)
  elsif records.respond_to? :to_ary
    records
  else
    records.all
  end
end

.apply_includes(records, directives) ⇒ Object



367
368
369
370
# File 'lib/jsonapi/resource.rb', line 367

def apply_includes(records, directives)
  records = records.includes(*directives.model_includes) if directives
  records
end

.apply_pagination(records, paginator, order_options) ⇒ Object



372
373
374
375
376
377
# File 'lib/jsonapi/resource.rb', line 372

def apply_pagination(records, paginator, order_options)
  if paginator
    records = paginator.apply(records, order_options)
  end
  records
end

.apply_sort(records, order_options) ⇒ Object



379
380
381
382
383
384
385
# File 'lib/jsonapi/resource.rb', line 379

def apply_sort(records, order_options)
  if order_options.any?
    records.order(order_options)
  else
    records
  end
end

.attribute(attr, options = {}) ⇒ Object



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/jsonapi/resource.rb', line 287

def attribute(attr, options = {})
  check_reserved_attribute_name(attr)

  if (attr.to_sym == :id) && (options[:format].nil?)
    ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
  end

  @_attributes ||= {}
  @_attributes[attr] = options
  define_method attr do
    @model.send(attr)
  end unless method_defined?(attr)

  define_method "#{attr}=" do |value|
    @model.send "#{attr}=", value
  end unless method_defined?("#{attr}=")
end

.attributes(*attrs) ⇒ Object

Methods used in defining a resource class



280
281
282
283
284
285
# File 'lib/jsonapi/resource.rb', line 280

def attributes(*attrs)
  options = attrs.extract_options!.dup
  attrs.each do |attr|
    attribute(attr, options)
  end
end

.construct_order_options(sort_params) ⇒ Object



584
585
586
587
588
589
590
591
# File 'lib/jsonapi/resource.rb', line 584

def construct_order_options(sort_params)
  return {} unless sort_params

  sort_params.each_with_object({}) { |sort, order_hash|
    field = sort[:field] == 'id' ? _primary_key : sort[:field]
    order_hash[field] = sort[:direction]
  }
end

.creatable_fields(context = nil) ⇒ Object

Override in your resource to filter the creatable keys



354
355
356
# File 'lib/jsonapi/resource.rb', line 354

def creatable_fields(context = nil)
  _updatable_associations | _attributes.keys
end

.create(context) ⇒ Object



263
264
265
# File 'lib/jsonapi/resource.rb', line 263

def create(context)
  self.new(self.create_model, context)
end

.create_modelObject



267
268
269
# File 'lib/jsonapi/resource.rb', line 267

def create_model
  _model_class.new
end

.default_attribute_optionsObject



305
306
307
# File 'lib/jsonapi/resource.rb', line 305

def default_attribute_options
  {format: :default}
end

.fieldsObject



363
364
365
# File 'lib/jsonapi/resource.rb', line 363

def fields
  _associations.keys | _attributes.keys
end

.filter(attr, *args) ⇒ Object



325
326
327
# File 'lib/jsonapi/resource.rb', line 325

def filter(attr, *args)
  @_allowed_filters[attr.to_sym] = args.extract_options!
end

.filter_records(filters, options) ⇒ Object



418
419
420
421
422
423
424
# File 'lib/jsonapi/resource.rb', line 418

def filter_records(filters, options)
  include_directives = options[:include_directives]

  records = records(options)
  records = apply_includes(records, include_directives)
  apply_filters(records, filters, options)
end

.filters(*attrs) ⇒ Object



321
322
323
# File 'lib/jsonapi/resource.rb', line 321

def filters(*attrs)
  @_allowed_filters.merge!(attrs.inject( Hash.new ) { |h, attr| h[attr] = {}; h })
end

.find(filters, options = {}) ⇒ Object

Override this method if you have more complex requirements than this basic find method provides



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/jsonapi/resource.rb', line 435

def find(filters, options = {})
  context = options[:context]

  records = filter_records(filters, options)

  sort_criteria = options.fetch(:sort_criteria) { [] }
  order_options = construct_order_options(sort_criteria)
  records = sort_records(records, order_options)

  records = apply_pagination(records, options[:paginator], order_options)

  resources = []
  records.each do |model|
    resources.push self.new(model, context)
  end

  return resources
end

.find_by_key(key, options = {}) ⇒ Object



454
455
456
457
458
459
460
461
462
463
464
# File 'lib/jsonapi/resource.rb', line 454

def find_by_key(key, options = {})
  context = options[:context]
  include_directives = options[:include_directives]
  records = records(options)
  records = apply_includes(records, include_directives)
  model = records.where({_primary_key => key}).first
  if model.nil?
    raise JSONAPI::Exceptions::RecordNotFound.new(key)
  end
  self.new(model, context)
end

.find_count(filters, options = {}) ⇒ Object



430
431
432
# File 'lib/jsonapi/resource.rb', line 430

def find_count(filters, options = {})
  filter_records(filters, options).count
end

.has_many(*attrs) ⇒ Object



313
314
315
# File 'lib/jsonapi/resource.rb', line 313

def has_many(*attrs)
  _associate(Association::HasMany, *attrs)
end

.has_one(*attrs) ⇒ Object



309
310
311
# File 'lib/jsonapi/resource.rb', line 309

def has_one(*attrs)
  _associate(Association::HasOne, *attrs)
end

.inherited(base) ⇒ Object



239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/jsonapi/resource.rb', line 239

def inherited(base)
  base._attributes = (_attributes || {}).dup
  base._associations = (_associations || {}).dup
  base._allowed_filters = (_allowed_filters || Set.new).dup

  type = base.name.demodulize.sub(/Resource$/, '').underscore
  base._type = type.pluralize.to_sym

  base.attribute :id, format: :id

  check_reserved_resource_name(base._type, base.name)
end

.is_filter_association?(filter) ⇒ Boolean

Returns:

  • (Boolean)


481
482
483
# File 'lib/jsonapi/resource.rb', line 481

def is_filter_association?(filter)
  filter == _type || _associations.include?(filter)
end

.method_missing(method, *args) ⇒ Object

TODO: remove this after the createable_fields and updateable_fields are phased out :nocov:



335
336
337
338
339
340
341
342
343
344
345
# File 'lib/jsonapi/resource.rb', line 335

def method_missing(method, *args)
  if method.to_s.match /createable_fields/
    ActiveSupport::Deprecation.warn("`createable_fields` is deprecated, please use `creatable_fields` instead")
    self.creatable_fields(*args)
  elsif method.to_s.match /updateable_fields/
    ActiveSupport::Deprecation.warn("`updateable_fields` is deprecated, please use `updatable_fields` instead")
    self.updatable_fields(*args)
  else
    super
  end
end

.model_name(model) ⇒ Object



317
318
319
# File 'lib/jsonapi/resource.rb', line 317

def model_name(model)
  @_model_name = model.to_sym
end

.module_pathObject



580
581
582
# File 'lib/jsonapi/resource.rb', line 580

def module_path
  @module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
end

.paginator(paginator) ⇒ Object



568
569
570
# File 'lib/jsonapi/resource.rb', line 568

def paginator(paginator)
  @_paginator = paginator
end

.primary_key(key) ⇒ Object



329
330
331
# File 'lib/jsonapi/resource.rb', line 329

def primary_key(key)
  @_primary_key = key.to_sym
end

.records(options = {}) ⇒ Object

Override this method if you want to customize the relation for finder methods (find, find_by_key)



468
469
470
# File 'lib/jsonapi/resource.rb', line 468

def records(options = {})
  _model_class
end

.resource_for(type) ⇒ Object



252
253
254
255
256
257
258
259
# File 'lib/jsonapi/resource.rb', line 252

def resource_for(type)
  resource_name = JSONAPI::Resource._resource_name_from_type(type)
  resource = resource_name.safe_constantize if resource_name
  if resource.nil?
    raise NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
  end
  resource
end

.routing_options(options) ⇒ Object



271
272
273
# File 'lib/jsonapi/resource.rb', line 271

def routing_options(options)
  @_routing_resource_options = options
end

.routing_resource_optionsObject



275
276
277
# File 'lib/jsonapi/resource.rb', line 275

def routing_resource_options
  @_routing_resource_options ||= {}
end

.sort_records(records, order_options) ⇒ Object



426
427
428
# File 'lib/jsonapi/resource.rb', line 426

def sort_records(records, order_options)
  apply_sort(records, order_options)
end

.sortable_fields(context = nil) ⇒ Object

Override in your resource to filter the sortable keys



359
360
361
# File 'lib/jsonapi/resource.rb', line 359

def sortable_fields(context = nil)
  _attributes.keys
end

.updatable_fields(context = nil) ⇒ Object

Override in your resource to filter the updatable keys



349
350
351
# File 'lib/jsonapi/resource.rb', line 349

def updatable_fields(context = nil)
  _updatable_associations | _attributes.keys - [:id]
end

.verify_association_filter(filter, raw, context = nil) ⇒ Object

override to allow for custom association logic, such as uuids, multiple keys or permission checks on keys



516
517
518
# File 'lib/jsonapi/resource.rb', line 516

def verify_association_filter(filter, raw, context = nil)
  return filter, raw
end

.verify_custom_filter(filter, value, context = nil) ⇒ Object

override to allow for custom filters



511
512
513
# File 'lib/jsonapi/resource.rb', line 511

def verify_custom_filter(filter, value, context = nil)
  return filter, value
end

.verify_filter(filter, raw, context = nil) ⇒ Object



485
486
487
488
489
490
491
492
493
494
# File 'lib/jsonapi/resource.rb', line 485

def verify_filter(filter, raw, context = nil)
  filter_values = []
  filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty?

  if is_filter_association?(filter)
    verify_association_filter(filter, filter_values, context)
  else
    verify_custom_filter(filter, filter_values, context)
  end
end

.verify_filters(filters, context = nil) ⇒ Object



472
473
474
475
476
477
478
479
# File 'lib/jsonapi/resource.rb', line 472

def verify_filters(filters, context = nil)
  verified_filters = {}
  filters.each do |filter, raw_value|
    verified_filter = verify_filter(filter, raw_value, context)
    verified_filters[verified_filter[0]] = verified_filter[1]
  end
  verified_filters
end

.verify_key(key, context = nil) ⇒ Object

override to allow for key processing and checking



497
498
499
500
501
# File 'lib/jsonapi/resource.rb', line 497

def verify_key(key, context = nil)
  key && Integer(key)
rescue
  raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
end

.verify_keys(keys, context = nil) ⇒ Object

override to allow for key processing and checking



504
505
506
507
508
# File 'lib/jsonapi/resource.rb', line 504

def verify_keys(keys, context = nil)
  return keys.collect do |key|
    verify_key(key, context)
  end
end

Instance Method Details

#change(callback) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/jsonapi/resource.rb', line 37

def change(callback)
  completed = false

  if @changing
    run_callbacks callback do
      completed = (yield == :completed)
    end
  else
    run_callbacks is_new? ? :create : :update do
      @changing = true
      run_callbacks callback do
        completed = (yield == :completed)
      end

      if @save_needed || is_new?
        completed = (save == :completed)
      end
    end
  end

  return completed ? :completed : :accepted
end


66
67
68
69
70
# File 'lib/jsonapi/resource.rb', line 66

def create_has_many_links(association_type, association_key_values)
  change :create_has_many_link do
    _create_has_many_links(association_type, association_key_values)
  end
end

#fetchable_fieldsObject

Override this on a resource instance to override the fetchable keys



103
104
105
# File 'lib/jsonapi/resource.rb', line 103

def fetchable_fields
  self.class.fields
end

#idObject



29
30
31
# File 'lib/jsonapi/resource.rb', line 29

def id
  model.send(self.class._primary_key)
end

#is_new?Boolean

Returns:

  • (Boolean)


33
34
35
# File 'lib/jsonapi/resource.rb', line 33

def is_new?
  id.nil?
end

#records_for(association_name, options = {}) ⇒ Object

Override this on a resource to customize how the associated records are fetched for a model. Particularly helpful for authorization.



109
110
111
# File 'lib/jsonapi/resource.rb', line 109

def records_for(association_name, options = {})
  model.send association_name
end

#removeObject



60
61
62
63
64
# File 'lib/jsonapi/resource.rb', line 60

def remove
  run_callbacks :remove do
    _remove
  end
end


84
85
86
87
88
# File 'lib/jsonapi/resource.rb', line 84

def remove_has_many_link(association_type, key)
  change :remove_has_many_link do
    _remove_has_many_link(association_type, key)
  end
end


90
91
92
93
94
# File 'lib/jsonapi/resource.rb', line 90

def remove_has_one_link(association_type)
  change :remove_has_one_link do
    _remove_has_one_link(association_type)
  end
end

#replace_fields(field_data) ⇒ Object



96
97
98
99
100
# File 'lib/jsonapi/resource.rb', line 96

def replace_fields(field_data)
  change :replace_fields do
    _replace_fields(field_data)
  end
end


72
73
74
75
76
# File 'lib/jsonapi/resource.rb', line 72

def replace_has_many_links(association_type, association_key_values)
  change :replace_has_many_links do
    _replace_has_many_links(association_type, association_key_values)
  end
end


78
79
80
81
82
# File 'lib/jsonapi/resource.rb', line 78

def replace_has_one_link(association_type, association_key_value)
  change :replace_has_one_link do
    _replace_has_one_link(association_type, association_key_value)
  end
end