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.



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.



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

def _allowed_filters
  @_allowed_filters
end

._attributesObject

Returns the value of attribute _attributes.



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

def _attributes
  @_attributes
end

._paginatorObject

Returns the value of attribute _paginator.



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

def _paginator
  @_paginator
end

._relationshipsObject

Returns the value of attribute _relationships.



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

def _relationships
  @_relationships
end

._typeObject

Returns the value of attribute _type.



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

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

._abstractObject



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

def _abstract
  @abstract
end

._allowed_filter?(filter) ⇒ Boolean

Returns:

  • (Boolean)


682
683
684
# File 'lib/jsonapi/resource.rb', line 682

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

._as_parent_keyObject



640
641
642
# File 'lib/jsonapi/resource.rb', line 640

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

._attribute_options(attr) ⇒ Object

quasi private class methods



614
615
616
# File 'lib/jsonapi/resource.rb', line 614

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

._has_relationship?(type) ⇒ Boolean

Returns:

  • (Boolean)


622
623
624
625
# File 'lib/jsonapi/resource.rb', line 622

def _has_relationship?(type)
  type = type.to_s
  @_relationships.key?(type.singularize.to_sym) || @_relationships.key?(type.pluralize.to_sym)
end

._model_classObject



673
674
675
676
677
678
679
680
# File 'lib/jsonapi/resource.rb', line 673

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



632
633
634
# File 'lib/jsonapi/resource.rb', line 632

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

._primary_keyObject



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

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

._relationship(type) ⇒ Object



627
628
629
630
# File 'lib/jsonapi/resource.rb', line 627

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

._resource_name_from_type(type) ⇒ Object



648
649
650
651
652
653
654
655
# File 'lib/jsonapi/resource.rb', line 648

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_relationshipsObject



618
619
620
# File 'lib/jsonapi/resource.rb', line 618

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

.abstract(val = true) ⇒ Object



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

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

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



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

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

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



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

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



424
425
426
427
428
429
430
431
432
# File 'lib/jsonapi/resource.rb', line 424

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



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

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

.apply_sort(records, order_options) ⇒ Object



439
440
441
442
443
444
445
# File 'lib/jsonapi/resource.rb', line 439

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

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



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

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



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

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

.construct_order_options(sort_params) ⇒ Object



690
691
692
693
694
695
696
697
# File 'lib/jsonapi/resource.rb', line 690

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



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

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

.create(context) ⇒ Object



285
286
287
# File 'lib/jsonapi/resource.rb', line 285

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

.create_modelObject



289
290
291
# File 'lib/jsonapi/resource.rb', line 289

def create_model
  _model_class.new
end

.default_attribute_optionsObject



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

def default_attribute_options
  { format: :default }
end

.fieldsObject



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

def fields
  _relationships.keys | _attributes.keys
end

.filter(attr, *args) ⇒ Object



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

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

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



476
477
478
479
# File 'lib/jsonapi/resource.rb', line 476

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

.filters(*attrs) ⇒ Object



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

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



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'lib/jsonapi/resource.rb', line 490

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



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

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



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

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

.has_many(*attrs) ⇒ Object



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

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

.has_one(*attrs) ⇒ Object



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

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

.inherited(base) ⇒ Object



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

def inherited(base)
  base.abstract(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)


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

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

.key_type(key_type) ⇒ Object



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

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:



372
373
374
375
376
377
378
379
380
381
382
# File 'lib/jsonapi/resource.rb', line 372

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



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

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

.module_pathObject



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

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

.paginator(paginator) ⇒ Object



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

def paginator(paginator)
  @_paginator = paginator
end

.primary_key(key) ⇒ Object



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

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)



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

def records(_options = {})
  _model_class
end

.relationship(*attrs) ⇒ Object



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

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



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/jsonapi/resource.rb', line 404

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(type) ⇒ Object



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

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

.resource_key_typeObject



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

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

.routing_options(options) ⇒ Object



293
294
295
# File 'lib/jsonapi/resource.rb', line 293

def routing_options(options)
  @_routing_resource_options = options
end

.routing_resource_optionsObject



297
298
299
# File 'lib/jsonapi/resource.rb', line 297

def routing_resource_options
  @_routing_resource_options ||= {}
end

.sort_records(records, order_options) ⇒ Object



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

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



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

def sortable_fields(_context = nil)
  _attributes.keys
end

.updatable_fields(_context = nil) ⇒ Object

Override in your resource to filter the updatable keys



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

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



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

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

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



537
538
539
540
541
542
543
544
545
546
# File 'lib/jsonapi/resource.rb', line 537

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



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

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



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
# File 'lib/jsonapi/resource.rb', line 556

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

  when :integer
    -> (key, context) {
      begin
        return key if key.nil?
        Integer(key)
      rescue
        raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
      end
    }
  when :string
    -> (key, context) {
      return key if key.nil?
      if key.to_s.include?(',')
        raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
      else
        key
      end
    }
  when :uuid
    -> (key, context) {
      return key 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
  end

  verification_proc.call(key, context)
rescue
  raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
end

.verify_keys(keys, context = nil) ⇒ Object

override to allow for key processing and checking



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

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



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

def verify_relationship_filter(filter, raw, _context = nil)
  [filter, raw]
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_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

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.public_send(self.class._primary_key)
end

#is_new?Boolean

Returns:

  • (Boolean)


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

def is_new?
  id.nil?
end

#records_for(relation_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(relation_name, _options = {})
  model.public_send relation_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_to_many_link(relationship_type, key)
  change :remove_to_many_link do
    _remove_to_many_link(relationship_type, key)
  end
end


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

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



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


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

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


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

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


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

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