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) ⇒ Resource

Returns a new instance of Resource.



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

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

Class Attribute Details

._allowed_filtersObject

Returns the value of attribute _allowed_filters.



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

def _allowed_filters
  @_allowed_filters
end

._attributesObject

Returns the value of attribute _attributes.



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

def _attributes
  @_attributes
end

._paginatorObject

Returns the value of attribute _paginator.



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

def _paginator
  @_paginator
end

._relationshipsObject

Returns the value of attribute _relationships.



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

def _relationships
  @_relationships
end

._typeObject

Returns the value of attribute _type.



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

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

Class Method Details

._abstractObject



693
694
695
# File 'lib/jsonapi/resource.rb', line 693

def _abstract
  @abstract
end

._allowed_filter?(filter) ⇒ Boolean

Returns:

  • (Boolean)


718
719
720
# File 'lib/jsonapi/resource.rb', line 718

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

._as_parent_keyObject



673
674
675
# File 'lib/jsonapi/resource.rb', line 673

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

._attribute_options(attr) ⇒ Object

quasi private class methods



652
653
654
# File 'lib/jsonapi/resource.rb', line 652

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

._immutableObject



701
702
703
# File 'lib/jsonapi/resource.rb', line 701

def _immutable
  @immutable
end

._model_classObject



709
710
711
712
713
714
715
716
# File 'lib/jsonapi/resource.rb', line 709

def _model_class
  return nil if _abstract

  return @model if @model
  @model = _model_name.to_s.safe_constantize
  warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this a base Resource declare it as abstract." if @model.nil?
  @model
end

._model_nameObject



665
666
667
# File 'lib/jsonapi/resource.rb', line 665

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

._primary_keyObject



669
670
671
# File 'lib/jsonapi/resource.rb', line 669

def _primary_key
  @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
end

._relationship(type) ⇒ Object



660
661
662
663
# File 'lib/jsonapi/resource.rb', line 660

def _relationship(type)
  type = type.to_sym
  @_relationships[type]
end

._updatable_relationshipsObject



656
657
658
# File 'lib/jsonapi/resource.rb', line 656

def _updatable_relationships
  @_relationships.map { |key, _relationship| key }
end

.abstract(val = true) ⇒ Object



689
690
691
# File 'lib/jsonapi/resource.rb', line 689

def abstract(val = true)
  @abstract = val
end

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



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

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

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



501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/jsonapi/resource.rb', line 501

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

  if filters
    filters.each do |filter, value|
      if _relationships.include?(filter)
        if _relationships[filter].is_a?(JSONAPI::Relationship::ToMany)
          required_includes.push(filter.to_s)
          records = apply_filter(records, "#{filter}.#{_relationships[filter].primary_key}", value, options)
        else
          records = apply_filter(records, "#{_relationships[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



474
475
476
477
478
479
480
481
482
# File 'lib/jsonapi/resource.rb', line 474

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

  records
end

.apply_pagination(records, paginator, order_options) ⇒ Object



484
485
486
487
# File 'lib/jsonapi/resource.rb', line 484

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

.apply_sort(records, order_options) ⇒ Object



489
490
491
492
493
494
495
# File 'lib/jsonapi/resource.rb', line 489

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

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



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/jsonapi/resource.rb', line 354

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.public_send(attr)
  end unless method_defined?(attr)

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

.attributes(*attrs) ⇒ Object

Methods used in defining a resource class



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

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

.construct_order_options(sort_params) ⇒ Object



726
727
728
729
730
731
732
733
# File 'lib/jsonapi/resource.rb', line 726

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



441
442
443
# File 'lib/jsonapi/resource.rb', line 441

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

.create(context) ⇒ Object



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

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

.create_modelObject



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

def create_model
  _model_class.new
end

.default_attribute_optionsObject



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

def default_attribute_options
  { format: :default }
end

.fetchable_fields(_context = nil) ⇒ Object

Override in your resource to filter the fetchable keys



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

def fetchable_fields(_context = nil)
  fields
end

.fieldsObject



450
451
452
# File 'lib/jsonapi/resource.rb', line 450

def fields
  _relationships.keys | _attributes.keys
end

.filter(attr, *args) ⇒ Object



407
408
409
# File 'lib/jsonapi/resource.rb', line 407

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

.filter_records(filters, options, records = records(options)) ⇒ Object



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

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

.filters(*attrs) ⇒ Object



403
404
405
# File 'lib/jsonapi/resource.rb', line 403

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



540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/jsonapi/resource.rb', line 540

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 resource_for_model_path(model, self.module_path).new(model, context)
  end

  resources
end

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



559
560
561
562
563
564
565
566
# File 'lib/jsonapi/resource.rb', line 559

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?
  resource_for_model_path(model, self.module_path).new(model, context)
end

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



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

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

.has_many(*attrs) ⇒ Object



395
396
397
# File 'lib/jsonapi/resource.rb', line 395

def has_many(*attrs)
  _add_relationship(Relationship::ToMany, *attrs)
end

.has_one(*attrs) ⇒ Object



391
392
393
# File 'lib/jsonapi/resource.rb', line 391

def has_one(*attrs)
  _add_relationship(Relationship::ToOne, *attrs)
end

.immutable(val = true) ⇒ Object



697
698
699
# File 'lib/jsonapi/resource.rb', line 697

def immutable(val = true)
  @immutable = val
end

.inherited(base) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/jsonapi/resource.rb', line 280

def inherited(base)
  base.abstract(false)
  base.immutable(false)
  base._attributes = (_attributes || {}).dup
  base._relationships = (_relationships || {}).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_relationship?(filter) ⇒ Boolean

Returns:

  • (Boolean)


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

def is_filter_relationship?(filter)
  filter == _type || _relationships.include?(filter)
end

.key_type(key_type) ⇒ Object



598
599
600
# File 'lib/jsonapi/resource.rb', line 598

def key_type(key_type)
  @_resource_key_type = key_type
end

.method_missing(method, *args) ⇒ Object

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



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

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



399
400
401
# File 'lib/jsonapi/resource.rb', line 399

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

.module_pathObject



722
723
724
# File 'lib/jsonapi/resource.rb', line 722

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

.mutable?Boolean

Returns:

  • (Boolean)


705
706
707
# File 'lib/jsonapi/resource.rb', line 705

def mutable?
  !@immutable
end

.paginator(paginator) ⇒ Object



685
686
687
# File 'lib/jsonapi/resource.rb', line 685

def paginator(paginator)
  @_paginator = paginator
end

.primary_key(key) ⇒ Object



411
412
413
# File 'lib/jsonapi/resource.rb', line 411

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)



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

def records(_options = {})
  _model_class
end

.relationship(*attrs) ⇒ Object



376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/jsonapi/resource.rb', line 376

def relationship(*attrs)
  options = attrs.extract_options!
  klass = case options[:to]
            when :one
              Relationship::ToOne
            when :many
              Relationship::ToMany
            else
              #:nocov:#
              fail ArgumentError.new('to: must be either :one or :many')
              #:nocov:#
          end
  _add_relationship(klass, *attrs, options.except(:to))
end

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



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/jsonapi/resource.rb', line 454

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

.resource_for(resource_path) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/jsonapi/resource.rb', line 295

def resource_for(resource_path)
  unless @@resource_types.key? resource_path
    klass_name = "#{resource_path.to_s.underscore.singularize}_resource".camelize
    klass = (klass_name.safe_constantize or
      fail NameError,
           "JSONAPI: Could not find resource '#{resource_path}'. (Class #{klass_name} not found)")
    normalized_path = resource_path.rpartition('/').first
    normalized_model = klass._model_name.to_s.gsub(/\A::/, '')
    @@resource_types[resource_path] = {
      resource: klass,
      path: normalized_path,
      model: normalized_model,
    }
  end
  @@resource_types[resource_path][:resource]
end

.resource_for_model_path(model, path) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/jsonapi/resource.rb', line 312

def resource_for_model_path(model, path)
  normalized_model = model.class.to_s.gsub(/\A::/, '')
  normalized_path = path.gsub(/\/\z/, '')
  resource = @@resource_types.find { |_, h|
    h[:path] == normalized_path && h[:model] == normalized_model
  }
  if resource
    resource.last[:resource]
  else
    #:nocov:#
    fail NameError,
         "JSONAPI: Could not find resource for model '#{path}#{normalized_model}'"
    #:nocov:#
  end
end

.resource_key_typeObject



602
603
604
# File 'lib/jsonapi/resource.rb', line 602

def resource_key_type
  @_resource_key_type || JSONAPI.configuration.resource_key_type
end

.routing_options(options) ⇒ Object



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

def routing_options(options)
  @_routing_resource_options = options
end

.routing_resource_optionsObject



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

def routing_resource_options
  @_routing_resource_options ||= {}
end

.sort_records(records, order_options) ⇒ Object



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

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



446
447
448
# File 'lib/jsonapi/resource.rb', line 446

def sortable_fields(_context = nil)
  _attributes.keys
end

.updatable_fields(_context = nil) ⇒ Object

Override in your resource to filter the updatable keys



436
437
438
# File 'lib/jsonapi/resource.rb', line 436

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

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

override to allow for custom filters



642
643
644
# File 'lib/jsonapi/resource.rb', line 642

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

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



587
588
589
590
591
592
593
594
595
596
# File 'lib/jsonapi/resource.rb', line 587

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

  if is_filter_relationship?(filter)
    verify_relationship_filter(filter, filter_values, context)
  else
    verify_custom_filter(filter, filter_values, context)
  end
end

.verify_filters(filters, context = nil) ⇒ Object



574
575
576
577
578
579
580
581
# File 'lib/jsonapi/resource.rb', line 574

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



606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
# File 'lib/jsonapi/resource.rb', line 606

def verify_key(key, context = nil)
  key_type = resource_key_type

  case key_type
  when :integer
    return if key.nil?
    Integer(key)
  when :string
    return if key.nil?
    if key.to_s.include?(',')
      raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
    else
      key
    end
  when :uuid
    return if key.nil?
    if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
      key
    else
      raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
    end
  else
    key_type.call(key, context)
  end
rescue
  raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
end

.verify_keys(keys, context = nil) ⇒ Object

override to allow for key processing and checking



635
636
637
638
639
# File 'lib/jsonapi/resource.rb', line 635

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

.verify_relationship_filter(filter, raw, _context = nil) ⇒ Object

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



647
648
649
# File 'lib/jsonapi/resource.rb', line 647

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

Instance Method Details

#_modelObject



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

def _model
  @model
end

#change(callback) ⇒ Object



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

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


68
69
70
71
72
# File 'lib/jsonapi/resource.rb', line 68

def create_to_many_links(relationship_type, relationship_key_values)
  change :create_to_many_link do
    _create_to_many_links(relationship_type, relationship_key_values)
  end
end

#fetchable_fieldsObject



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

def fetchable_fields
  self.class.fetchable_fields(context)
end

#idObject



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

def id
  _model.public_send(self.class._primary_key)
end

#is_new?Boolean

Returns:

  • (Boolean)


37
38
39
# File 'lib/jsonapi/resource.rb', line 37

def is_new?
  id.nil?
end

#meta(_options) ⇒ Object

Override this to return resource level meta data must return a hash, and if the hash is empty the meta section will not be serialized with the resource meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the serializer’s format_key and format_value methods if desired the _options hash will contain the serializer and the serialization_options



129
130
131
# File 'lib/jsonapi/resource.rb', line 129

def meta(_options)
  {}
end

#model_error_messagesObject



120
121
122
# File 'lib/jsonapi/resource.rb', line 120

def model_error_messages
  _model.errors.messages
end

#records_for(relation_name) ⇒ Object

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



116
117
118
# File 'lib/jsonapi/resource.rb', line 116

def records_for(relation_name)
  _model.public_send relation_name
end

#removeObject



62
63
64
65
66
# File 'lib/jsonapi/resource.rb', line 62

def remove
  run_callbacks :remove do
    _remove
  end
end


92
93
94
95
96
# File 'lib/jsonapi/resource.rb', line 92

def remove_to_many_link(relationship_type, key)
  change :remove_to_many_link do
    _remove_to_many_link(relationship_type, key)
  end
end


98
99
100
101
102
# File 'lib/jsonapi/resource.rb', line 98

def remove_to_one_link(relationship_type)
  change :remove_to_one_link do
    _remove_to_one_link(relationship_type)
  end
end

#replace_fields(field_data) ⇒ Object



104
105
106
107
108
# File 'lib/jsonapi/resource.rb', line 104

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


86
87
88
89
90
# File 'lib/jsonapi/resource.rb', line 86

def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
  change :replace_polymorphic_to_one_link do
    _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
  end
end


74
75
76
77
78
# File 'lib/jsonapi/resource.rb', line 74

def replace_to_many_links(relationship_type, relationship_key_values)
  change :replace_to_many_links do
    _replace_to_many_links(relationship_type, relationship_key_values)
  end
end


80
81
82
83
84
# File 'lib/jsonapi/resource.rb', line 80

def replace_to_one_link(relationship_type, relationship_key_value)
  change :replace_to_one_link do
    _replace_to_one_link(relationship_type, relationship_key_value)
  end
end