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



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

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

Class Attribute Details

._allowed_filtersObject

Returns the value of attribute _allowed_filters.



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

def _allowed_filters
  @_allowed_filters
end

._associationsObject

Returns the value of attribute _associations.



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

def _associations
  @_associations
end

._attributesObject

Returns the value of attribute _attributes.



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

def _attributes
  @_attributes
end

._paginatorObject

Returns the value of attribute _paginator.



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

def _paginator
  @_paginator
end

._typeObject

Returns the value of attribute _type.



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

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



612
613
614
# File 'lib/jsonapi/resource.rb', line 612

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

._as_parent_keyObject



583
584
585
# File 'lib/jsonapi/resource.rb', line 583

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

._association(type) ⇒ Object



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

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

._attribute_options(attr) ⇒ Object

quasi private class methods



557
558
559
# File 'lib/jsonapi/resource.rb', line 557

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

._has_association?(type) ⇒ Boolean



565
566
567
568
# File 'lib/jsonapi/resource.rb', line 565

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

._model_classObject



608
609
610
# File 'lib/jsonapi/resource.rb', line 608

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

._model_nameObject



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

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

._primary_keyObject



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

def _primary_key
  @_primary_key ||= :id
end

._resource_name_from_type(type) ⇒ Object



591
592
593
594
595
596
597
598
# File 'lib/jsonapi/resource.rb', line 591

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

._updatable_associationsObject



561
562
563
# File 'lib/jsonapi/resource.rb', line 561

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

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



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

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

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



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

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.to_s)
          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 = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes)))
  end

  records
end

.apply_includes(records, options = {}) ⇒ Object



407
408
409
410
411
412
413
414
415
# File 'lib/jsonapi/resource.rb', line 407

def apply_includes(records, options = {})
  include_directives = options[:include_directives]
  if include_directives
    model_includes = resolve_association_names_to_relations(self, include_directives.model_includes, options)
    records = records.includes(model_includes)
  end

  records
end

.apply_pagination(records, paginator, order_options) ⇒ Object



417
418
419
420
# File 'lib/jsonapi/resource.rb', line 417

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

.apply_sort(records, order_options) ⇒ Object



422
423
424
425
426
427
428
# File 'lib/jsonapi/resource.rb', line 422

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

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



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/jsonapi/resource.rb', line 307

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



300
301
302
303
304
305
# File 'lib/jsonapi/resource.rb', line 300

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

.construct_order_options(sort_params) ⇒ Object



620
621
622
623
624
625
626
627
# File 'lib/jsonapi/resource.rb', line 620

def construct_order_options(sort_params)
  return {} unless sort_params

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

.creatable_fields(_context = nil) ⇒ Object

Override in your resource to filter the creatable keys



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

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

.create(context) ⇒ Object



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

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

.create_modelObject



287
288
289
# File 'lib/jsonapi/resource.rb', line 287

def create_model
  _model_class.new
end

.default_attribute_optionsObject



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

def default_attribute_options
  { format: :default }
end

.fieldsObject



383
384
385
# File 'lib/jsonapi/resource.rb', line 383

def fields
  _associations.keys | _attributes.keys
end

.filter(attr, *args) ⇒ Object



345
346
347
# File 'lib/jsonapi/resource.rb', line 345

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

.filter_records(filters, options) ⇒ Object



459
460
461
462
463
# File 'lib/jsonapi/resource.rb', line 459

def filter_records(filters, options)
  records = records(options)
  records = apply_filters(records, filters, options)
  apply_includes(records, options)
end

.filters(*attrs) ⇒ Object



341
342
343
# File 'lib/jsonapi/resource.rb', line 341

def filters(*attrs)
  @_allowed_filters.merge!(attrs.inject({}) { |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



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/jsonapi/resource.rb', line 474

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 new(model, context)
  end

  resources
end

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



493
494
495
496
497
498
499
500
# File 'lib/jsonapi/resource.rb', line 493

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

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



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

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

.has_many(*attrs) ⇒ Object



333
334
335
# File 'lib/jsonapi/resource.rb', line 333

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

.has_one(*attrs) ⇒ Object



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

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

.inherited(base) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/jsonapi/resource.rb', line 259

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



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

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:



355
356
357
358
359
360
361
362
363
364
365
# File 'lib/jsonapi/resource.rb', line 355

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

.model_name(model) ⇒ Object



337
338
339
# File 'lib/jsonapi/resource.rb', line 337

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

.module_pathObject



616
617
618
# File 'lib/jsonapi/resource.rb', line 616

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

.paginator(paginator) ⇒ Object



604
605
606
# File 'lib/jsonapi/resource.rb', line 604

def paginator(paginator)
  @_paginator = paginator
end

.primary_key(key) ⇒ Object



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

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)



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

def records(_options = {})
  _model_class
end

.resolve_association_names_to_relations(resource_klass, model_includes, options = {}) ⇒ Object



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/jsonapi/resource.rb', line 387

def resolve_association_names_to_relations(resource_klass, model_includes, options = {})
  case model_includes
    when Array
      return model_includes.map do |value|
        resolve_association_names_to_relations(resource_klass, value, options)
      end
    when Hash
      model_includes.keys.each do |key|
        association = resource_klass._associations[key]
        value = model_includes[key]
        model_includes.delete(key)
        model_includes[association.relation_name(options)] = resolve_association_names_to_relations(association.resource_klass, value, options)
      end
      return model_includes
    when Symbol
      association = resource_klass._associations[model_includes]
      return association.relation_name(options)
  end
end

.resource_for(type) ⇒ Object



272
273
274
275
276
277
278
279
# File 'lib/jsonapi/resource.rb', line 272

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

.routing_options(options) ⇒ Object



291
292
293
# File 'lib/jsonapi/resource.rb', line 291

def routing_options(options)
  @_routing_resource_options = options
end

.routing_resource_optionsObject



295
296
297
# File 'lib/jsonapi/resource.rb', line 295

def routing_resource_options
  @_routing_resource_options ||= {}
end

.sort_records(records, order_options) ⇒ Object



465
466
467
# File 'lib/jsonapi/resource.rb', line 465

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



379
380
381
# File 'lib/jsonapi/resource.rb', line 379

def sortable_fields(_context = nil)
  _attributes.keys
end

.updatable_fields(_context = nil) ⇒ Object

Override in your resource to filter the updatable keys



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

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



552
553
554
# File 'lib/jsonapi/resource.rb', line 552

def verify_association_filter(filter, raw, _context = nil)
  [filter, raw]
end

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

override to allow for custom filters



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

def verify_custom_filter(filter, value, _context = nil)
  [filter, value]
end

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



521
522
523
524
525
526
527
528
529
530
# File 'lib/jsonapi/resource.rb', line 521

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



508
509
510
511
512
513
514
515
# File 'lib/jsonapi/resource.rb', line 508

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



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

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



540
541
542
543
544
# File 'lib/jsonapi/resource.rb', line 540

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

Instance Method Details

#change(callback) ⇒ Object



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

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

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

  return completed ? :completed : :accepted
end


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

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



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

def fetchable_fields
  self.class.fields
end

#idObject



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

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

#is_new?Boolean



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

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.



114
115
116
# File 'lib/jsonapi/resource.rb', line 114

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

#removeObject



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

def remove
  run_callbacks :remove do
    _remove
  end
end


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

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


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

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



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

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


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

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


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

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


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

def replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
  change :replace_polymorphic_has_one_link do
    _replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
  end
end