jsonapi-resources 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 233ad0db650881125288636b8fc022ccace40b01
4
- data.tar.gz: 9c28dc89ef3f227398d6eecb9e4e274bcbd40d59
3
+ metadata.gz: 32e7bab3bce04e8ef68e0e58fd0274b128d7b68c
4
+ data.tar.gz: d12938d652c1ea0c692fea2c940354e673f75678
5
5
  SHA512:
6
- metadata.gz: 57bca518364bab67494516f10166275905eeb115709113601eb96c691d6bbf1ee2d1db4aa0c17ec83093f746cc8d38f37d4f8f11e1b59b2a2ae68a35922585ca
7
- data.tar.gz: 30c3a7125c2eccbcda03e0daa8adab71ac8542afa599d8787087ee9fa461aa1efaff9e95fc8544f6762b8405000d82cec1911adb6d3ec7e7a1e269f60bf387e2
6
+ metadata.gz: dd1e03599ab80a412cbe06e8d8d3231131a221f3406d82432c05d1b00c38b5002b3f1b30791c23328e613f12638f55ca35177439465ee1359dcb62f96a256057
7
+ data.tar.gz: 499fcf89189161652538b1716db928efe228b0db2e0d377012a0f98439463c1ef334cfc18b842a737f71e8c53aa9f2925932035cdbef3573726edcfb780d464b
data/README.md CHANGED
@@ -340,7 +340,8 @@ end
340
340
 
341
341
  ##### Custom resource key validators
342
342
 
343
- If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that accepts key and context arguments in `config/initializers/jsonapi_resources.rb`:
343
+ If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that
344
+ accepts key and context arguments in `config/initializers/jsonapi_resources.rb`:
344
345
 
345
346
  ```ruby
346
347
  JSONAPI.configure do |config|
@@ -361,6 +362,38 @@ class AuthorResource < JSONAPI::Resource
361
362
  end
362
363
  ```
363
364
 
365
+ #### Model Hints
366
+
367
+ Resource instances are created from model records. The determination of the correct resource type is performed using a
368
+ simple rule based on the model's name. The name is used to find a resource in the same module (as the originating
369
+ resource) that matches the name. This usually works quite well, however it can fail when model names do not match
370
+ resource names. It can also fail when using namespaced models. In this case a `model_hint` can be created to map model
371
+ names to resources. For example:
372
+
373
+ ```ruby
374
+ class AuthorResource < JSONAPI::Resource
375
+ attribute :name
376
+ model_name 'Person'
377
+ model_hint model: Commenter, resource: :special_person
378
+
379
+ has_many :posts
380
+ has_many :commenters
381
+ end
382
+ ```
383
+
384
+ Note that when `model_name` is set a corresponding `model_hint` is also added. This can be skipped by using the
385
+ `add_model_hint` option set to false. For example:
386
+
387
+ ```ruby
388
+ class AuthorResource < JSONAPI::Resource
389
+ model_name 'Legacy::Person', add_model_hint: false
390
+ end
391
+ ```
392
+
393
+ Model hints inherit from parent resources, but are not global in scope. The `model_hint` method accepts `model` and
394
+ `resource` named parameters. `model` takes an ActiveRecord class or class name (defaults to the model name), and
395
+ `resource` takes a resource type or a resource class (defaults to the current resource's type).
396
+
364
397
  #### Relationships
365
398
 
366
399
  Related resources need to be specified in the resource. These may be declared with the `relationship` or the `has_one`
@@ -509,11 +542,65 @@ end
509
542
 
510
543
  The default value is used as if it came from the request.
511
544
 
545
+ ##### Applying Filters
546
+
547
+ You may customize how a filter behaves by supplying a callable to the `:apply` option. This callable will be used to
548
+ apply that filter. The callable is passed the `records`, which is an `ActiveRecord::Relation`, the `value`, and an
549
+ `_options` hash. It is expected to return an `ActiveRecord::Relation`.
550
+
551
+ This example shows how you can implement different approaches for different filters.
552
+
553
+ ```ruby
554
+ filter :visibility, apply: ->(records, value, _options) {
555
+ records.where('users.publicly_visible = ?', value == :public)
556
+ }
557
+ ```
558
+
559
+ If you omit the `apply` callable the filter will be applied as `records.where(filter => value)`.
560
+
561
+ Note: It is also possible to override the `self.apply_filter` method, though this approach is now deprecated:
562
+
563
+ ```ruby
564
+ def self.apply_filter(records, filter, value, options)
565
+ case filter
566
+ when :last_name, :first_name, :name
567
+ if value.is_a?(Array)
568
+ value.each do |val|
569
+ records = records.where(_model_class.arel_table[filter].matches(val))
570
+ end
571
+ return records
572
+ else
573
+ records.where(_model_class.arel_table[filter].matches(value))
574
+ end
575
+ else
576
+ return super(records, filter, value)
577
+ end
578
+ end
579
+ ```
580
+
581
+ ##### Verifying Filters
582
+
583
+ Because filters typically come straight from the request, it's prudent to verify their values. To do so, provide a
584
+ callable to the `verify` option. This callable will be passed the `value` and the `context`. Verify should return the
585
+ verified value, which may be modified.
586
+
587
+ ```ruby
588
+ filter :ids,
589
+ verify: ->(values, context) {
590
+ verify_keys(values, context)
591
+ return values
592
+ },
593
+ apply: -> (records, value, _options) {
594
+ records.where('id IN (?)', value)
595
+ }
596
+ ```
597
+
512
598
  ##### Finders
513
599
 
514
600
  Basic finding by filters is supported by resources. This is implemented in the `find` and `find_by_key` finder methods.
515
601
  Currently this is implemented for `ActiveRecord` based resources. The finder methods rely on the `records` method to get
516
- an `Arel` relation. It is therefore possible to override `records` to affect the three find related methods.
602
+ an `ActiveRecord::Relation` relation. It is therefore possible to override `records` to affect the three find related
603
+ methods.
517
604
 
518
605
  ###### Customizing base records for finder methods
519
606
 
@@ -572,7 +659,6 @@ class BaseResource < JSONAPI::Resource
572
659
  end
573
660
  ```
574
661
 
575
-
576
662
  ###### Raising Errors
577
663
 
578
664
  Inside the finder methods (like `records_for`) or inside of resource callbacks
@@ -635,6 +721,26 @@ def self.apply_filter(records, filter, value, options)
635
721
  end
636
722
  ```
637
723
 
724
+
725
+ ###### Applying Sorting
726
+
727
+ You can override the `apply_sort` method to gain control over how the sorting is done. This may be useful in case you'd
728
+ like to base the sorting on variables in your context.
729
+
730
+ Example:
731
+
732
+ ```ruby
733
+ def self.apply_sort(records, order_options, context = {})
734
+ if order_options.has?(:trending)
735
+ records = records.order_by_trending_scope
736
+ order_options - [:trending]
737
+ end
738
+
739
+ super(records, order_options, context)
740
+ end
741
+ ```
742
+
743
+
638
744
  ###### Override finder methods
639
745
 
640
746
  Finally if you have more complex requirements for finding you can override the `find` and `find_by_key` methods on the
@@ -64,12 +64,12 @@ module JSONAPI
64
64
  render_results(operation_results)
65
65
  end
66
66
 
67
+ rescue => e
68
+ handle_exceptions(e)
69
+ ensure
67
70
  if response.body.size > 0
68
71
  response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
69
72
  end
70
-
71
- rescue => e
72
- handle_exceptions(e)
73
73
  end
74
74
 
75
75
  # set the operations processor in the configuration or override this to use another operations processor
@@ -178,10 +178,10 @@ module JSONAPI
178
178
  case e
179
179
  when JSONAPI::Exceptions::Error
180
180
  render_errors(e.errors)
181
- else # raise all other exceptions
182
- # :nocov:
183
- fail e
184
- # :nocov:
181
+ else
182
+ internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
183
+ Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
184
+ render_errors(internal_server_error.errors)
185
185
  end
186
186
  end
187
187
 
@@ -1,6 +1,6 @@
1
1
  module JSONAPI
2
2
  class Error
3
- attr_accessor :title, :detail, :id, :href, :code, :source, :links, :status
3
+ attr_accessor :title, :detail, :id, :href, :code, :source, :links, :status, :meta
4
4
 
5
5
  def initialize(options = {})
6
6
  @title = options[:title]
@@ -16,6 +16,7 @@ module JSONAPI
16
16
  @links = options[:links]
17
17
 
18
18
  @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[options[:status]].to_s
19
+ @meta = options[:meta]
19
20
  end
20
21
  end
21
22
 
@@ -10,10 +10,17 @@ module JSONAPI
10
10
  end
11
11
 
12
12
  def errors
13
+ unless Rails.env.production?
14
+ meta = Hash.new
15
+ meta[:exception] = exception.message
16
+ meta[:backtrace] = exception.backtrace
17
+ end
18
+
13
19
  [JSONAPI::Error.new(code: JSONAPI::INTERNAL_SERVER_ERROR,
14
20
  status: :internal_server_error,
15
21
  title: 'Internal Server Error',
16
- detail: 'Internal Server Error')]
22
+ detail: 'Internal Server Error',
23
+ meta: meta)]
17
24
  end
18
25
  end
19
26
 
@@ -116,7 +116,7 @@ module JSONAPI
116
116
  def regular_primary_resources_path
117
117
  [
118
118
  formatted_module_path_from_class(primary_resource_klass),
119
- route_formatter.format(primary_resource_klass._type.to_s),
119
+ format_route(primary_resource_klass._type.to_s),
120
120
  ].join
121
121
  end
122
122
 
@@ -127,7 +127,7 @@ module JSONAPI
127
127
  def regular_resource_path(source)
128
128
  [
129
129
  formatted_module_path_from_class(source.class),
130
- route_formatter.format(source.class._type.to_s),
130
+ format_route(source.class._type.to_s),
131
131
  "/#{ source.id }",
132
132
  ].join
133
133
  end
@@ -1,14 +1,15 @@
1
1
  module JSONAPI
2
2
  class Relationship
3
3
  attr_reader :acts_as_set, :foreign_key, :type, :options, :name,
4
- :class_name, :polymorphic, :always_include_linkage_data
4
+ :class_name, :polymorphic, :always_include_linkage_data,
5
+ :parent_resource
5
6
 
6
7
  def initialize(name, options = {})
7
8
  @name = name.to_s
8
9
  @options = options
9
10
  @acts_as_set = options.fetch(:acts_as_set, false) == true
10
11
  @foreign_key = options[:foreign_key] ? options[:foreign_key].to_sym : nil
11
- @module_path = options[:module_path] || ''
12
+ @parent_resource = options[:parent_resource]
12
13
  @relation_name = options.fetch(:relation_name, @name)
13
14
  @polymorphic = options.fetch(:polymorphic, false) == true
14
15
  @always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
@@ -21,7 +22,7 @@ module JSONAPI
21
22
  end
22
23
 
23
24
  def resource_klass
24
- @resource_klass ||= Resource.resource_for(@module_path + @class_name)
25
+ @resource_klass = @parent_resource.resource_for(@class_name)
25
26
  end
26
27
 
27
28
  def relation_name(options)
@@ -463,7 +463,7 @@ module JSONAPI
463
463
 
464
464
  unless links_object[:id].nil?
465
465
  resource = self.resource_klass || Resource
466
- relationship_resource = resource.resource_for(@resource_klass.module_path + unformat_key(links_object[:type]).to_s)
466
+ relationship_resource = resource.resource_for(unformat_key(links_object[:type]).to_s)
467
467
  relationship_id = relationship_resource.verify_key(links_object[:id], @context)
468
468
  if relationship.polymorphic?
469
469
  { id: relationship_id, type: unformat_key(links_object[:type].to_s) }
@@ -610,11 +610,6 @@ module JSONAPI
610
610
  def parse_single_replace_operation(data, keys, id_key_presence_check_required: true)
611
611
  fail JSONAPI::Exceptions::MissingKey.new if data[:id].nil?
612
612
 
613
- type = data[:type]
614
- if type.nil? || type != format_key(@resource_klass._type).to_s
615
- fail JSONAPI::Exceptions::ParameterMissing.new(:type)
616
- end
617
-
618
613
  key = data[:id]
619
614
  if id_key_presence_check_required && !keys.include?(key)
620
615
  fail JSONAPI::Exceptions::KeyNotIncludedInURL.new(key)
@@ -4,8 +4,6 @@ module JSONAPI
4
4
  class Resource
5
5
  include Callbacks
6
6
 
7
- @@resource_types = {}
8
-
9
7
  attr_reader :context
10
8
 
11
9
  define_jsonapi_resources_callbacks :create,
@@ -277,55 +275,61 @@ module JSONAPI
277
275
  end
278
276
 
279
277
  class << self
280
- def inherited(base)
281
- base.abstract(false)
282
- base.immutable(false)
283
- base._attributes = (_attributes || {}).dup
284
- base._relationships = (_relationships || {}).dup
285
- base._allowed_filters = (_allowed_filters || Set.new).dup
286
-
287
- type = base.name.demodulize.sub(/Resource$/, '').underscore
288
- base._type = type.pluralize.to_sym
289
-
290
- base.attribute :id, format: :id
291
-
292
- check_reserved_resource_name(base._type, base.name)
293
- end
294
-
295
- def resource_for(resource_path)
296
- unless @@resource_types.key? resource_path
297
- klass_name = "#{resource_path.to_s.underscore.singularize}_resource".camelize
298
- klass = (klass_name.safe_constantize or
299
- fail NameError,
300
- "JSONAPI: Could not find resource '#{resource_path}'. (Class #{klass_name} not found)")
301
- normalized_path = resource_path.rpartition('/').first
302
- normalized_model = klass._model_name.to_s.gsub(/\A::/, '')
303
- @@resource_types[resource_path] = {
304
- resource: klass,
305
- path: normalized_path,
306
- model: normalized_model,
307
- }
278
+ def inherited(subclass)
279
+ subclass.abstract(false)
280
+ subclass.immutable(false)
281
+ subclass._attributes = (_attributes || {}).dup
282
+ subclass._model_hints = (_model_hints || {}).dup
283
+
284
+ subclass._relationships = {}
285
+ # Add the relationships from the base class to the subclass using the original options
286
+ if _relationships.is_a?(Hash)
287
+ _relationships.each_value do |relationship|
288
+ options = relationship.options.dup
289
+ options[:parent_resource] = subclass
290
+ subclass._add_relationship(relationship.class, relationship.name, options)
291
+ end
292
+ end
293
+
294
+ subclass._allowed_filters = (_allowed_filters || Set.new).dup
295
+
296
+ type = subclass.name.demodulize.sub(/Resource$/, '').underscore
297
+ subclass._type = type.pluralize.to_sym
298
+
299
+ subclass.attribute :id, format: :id
300
+
301
+ check_reserved_resource_name(subclass._type, subclass.name)
302
+ end
303
+
304
+ def resource_for(type)
305
+ type_with_module = type.include?('/') ? type : module_path + type
306
+
307
+ resource_name = _resource_name_from_type(type_with_module)
308
+ resource = resource_name.safe_constantize if resource_name
309
+ if resource.nil?
310
+ fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
308
311
  end
309
- @@resource_types[resource_path][:resource]
312
+ resource
310
313
  end
311
314
 
312
- def resource_for_model_path(model, path)
313
- normalized_model = model.class.to_s.gsub(/\A::/, '')
314
- normalized_path = path.gsub(/\/\z/, '')
315
- resource = @@resource_types.find { |_, h|
316
- h[:path] == normalized_path && h[:model] == normalized_model
317
- }
318
- if resource
319
- resource.last[:resource]
315
+ def resource_for_model(model)
316
+ resource_for(resource_type_for(model))
317
+ end
318
+
319
+ def _resource_name_from_type(type)
320
+ "#{type.to_s.underscore.singularize}_resource".camelize
321
+ end
322
+
323
+ def resource_type_for(model)
324
+ model_name = model.class.to_s.underscore
325
+ if _model_hints[model_name]
326
+ _model_hints[model_name]
320
327
  else
321
- #:nocov:#
322
- fail NameError,
323
- "JSONAPI: Could not find resource for model '#{path}#{normalized_model}'"
324
- #:nocov:#
328
+ model_name.rpartition('/').last
325
329
  end
326
330
  end
327
331
 
328
- attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator
332
+ attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints
329
333
 
330
334
  def create(context)
331
335
  new(create_model, context)
@@ -396,8 +400,17 @@ module JSONAPI
396
400
  _add_relationship(Relationship::ToMany, *attrs)
397
401
  end
398
402
 
399
- def model_name(model)
403
+ def model_name(model, options = {})
400
404
  @_model_name = model.to_sym
405
+
406
+ model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
407
+ end
408
+
409
+ def model_hint(model: _model_name, resource: _type)
410
+ model_name = ((model.is_a?(Class)) && (model < ActiveRecord::Base)) ? model.name : model
411
+ resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
412
+
413
+ _model_hints[model_name.to_s.gsub('::', '/').underscore] = resource_type.to_s
401
414
  end
402
415
 
403
416
  def filters(*attrs)
@@ -486,7 +499,7 @@ module JSONAPI
486
499
  records
487
500
  end
488
501
 
489
- def apply_sort(records, order_options)
502
+ def apply_sort(records, order_options, _context = {})
490
503
  if order_options.any?
491
504
  records.order(order_options)
492
505
  else
@@ -494,8 +507,14 @@ module JSONAPI
494
507
  end
495
508
  end
496
509
 
497
- def apply_filter(records, filter, value, _options = {})
498
- records.where(filter => value)
510
+ def apply_filter(records, filter, value, options = {})
511
+ strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
512
+
513
+ if strategy
514
+ strategy.call(records, value, options)
515
+ else
516
+ records.where(filter => value)
517
+ end
499
518
  end
500
519
 
501
520
  def apply_filters(records, filters, options = {})
@@ -528,8 +547,8 @@ module JSONAPI
528
547
  apply_includes(records, options)
529
548
  end
530
549
 
531
- def sort_records(records, order_options)
532
- apply_sort(records, order_options)
550
+ def sort_records(records, order_options, context = {})
551
+ apply_sort(records, order_options, context)
533
552
  end
534
553
 
535
554
  def find_count(filters, options = {})
@@ -544,13 +563,13 @@ module JSONAPI
544
563
 
545
564
  sort_criteria = options.fetch(:sort_criteria) { [] }
546
565
  order_options = construct_order_options(sort_criteria)
547
- records = sort_records(records, order_options)
566
+ records = sort_records(records, order_options, context)
548
567
 
549
568
  records = apply_pagination(records, options[:paginator], order_options)
550
569
 
551
570
  resources = []
552
571
  records.each do |model|
553
- resources.push resource_for_model_path(model, self.module_path).new(model, context)
572
+ resources.push self.resource_for_model(model).new(model, context)
554
573
  end
555
574
 
556
575
  resources
@@ -562,7 +581,7 @@ module JSONAPI
562
581
  records = apply_includes(records, options)
563
582
  model = records.where({_primary_key => key}).first
564
583
  fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
565
- resource_for_model_path(model, self.module_path).new(model, context)
584
+ self.resource_for_model(model).new(model, context)
566
585
  end
567
586
 
568
587
  # Override this method if you want to customize the relation for
@@ -588,10 +607,16 @@ module JSONAPI
588
607
  filter_values = []
589
608
  filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty?
590
609
 
591
- if is_filter_relationship?(filter)
592
- verify_relationship_filter(filter, filter_values, context)
610
+ strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
611
+
612
+ if strategy
613
+ [filter, strategy.call(filter_values, context)]
593
614
  else
594
- verify_custom_filter(filter, filter_values, context)
615
+ if is_filter_relationship?(filter)
616
+ verify_relationship_filter(filter, filter_values, context)
617
+ else
618
+ verify_custom_filter(filter, filter_values, context)
619
+ end
595
620
  end
596
621
  end
597
622
 
@@ -638,12 +663,13 @@ module JSONAPI
638
663
  end
639
664
  end
640
665
 
641
- # override to allow for custom filters
666
+ # Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
642
667
  def verify_custom_filter(filter, value, _context = nil)
643
668
  [filter, value]
644
669
  end
645
670
 
646
- # override to allow for custom relationship logic, such as uuids, multiple keys or permission checks on keys
671
+ # Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
672
+ # relationship logic, such as uuids, multiple keys or permission checks on keys
647
673
  def verify_relationship_filter(filter, raw, _context = nil)
648
674
  [filter, raw]
649
675
  end
@@ -663,7 +689,7 @@ module JSONAPI
663
689
  end
664
690
 
665
691
  def _model_name
666
- @_model_name ||= name.demodulize.sub(/Resource$/, '')
692
+ _abstract ? '' : @_model_name ||= name.demodulize.sub(/Resource$/, '')
667
693
  end
668
694
 
669
695
  def _primary_key
@@ -720,7 +746,11 @@ module JSONAPI
720
746
  end
721
747
 
722
748
  def module_path
723
- name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
749
+ if name == 'JSONAPI::Resource'
750
+ ''
751
+ else
752
+ name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
753
+ end
724
754
  end
725
755
 
726
756
  def construct_order_options(sort_params)
@@ -732,49 +762,28 @@ module JSONAPI
732
762
  end
733
763
  end
734
764
 
735
- private
736
-
737
- def check_reserved_resource_name(type, name)
738
- if [:ids, :types, :hrefs, :links].include?(type)
739
- warn "[NAME COLLISION] `#{name}` is a reserved resource name."
740
- return
741
- end
742
- end
743
-
744
- def check_reserved_attribute_name(name)
745
- # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
746
- # an attribute method won't be created for it.
747
- if [:type].include?(name.to_sym)
748
- warn "[NAME COLLISION] `#{name}` is a reserved key in #{@@resource_types[_type]}."
749
- end
750
- end
751
-
752
- def check_reserved_relationship_name(name)
753
- if [:id, :ids, :type, :types].include?(name.to_sym)
754
- warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{@@resource_types[_type]}."
755
- end
756
- end
757
-
758
765
  def _add_relationship(klass, *attrs)
759
766
  options = attrs.extract_options!
760
- options[:module_path] = module_path
767
+ options[:parent_resource] = self
761
768
 
762
769
  attrs.each do |attr|
763
- check_reserved_relationship_name(attr)
770
+ relationship_name = attr.to_sym
771
+
772
+ check_reserved_relationship_name(relationship_name)
764
773
 
765
774
  # Initialize from an ActiveRecord model's properties
766
775
  if _model_class && _model_class.ancestors.collect{|ancestor| ancestor.name}.include?('ActiveRecord::Base')
767
- model_association = _model_class.reflect_on_association(attr)
776
+ model_association = _model_class.reflect_on_association(relationship_name)
768
777
  if model_association
769
778
  options[:class_name] ||= model_association.class_name
770
779
  end
771
780
  end
772
781
 
773
- @_relationships[attr] = relationship = klass.new(attr, options)
782
+ @_relationships[relationship_name] = relationship = klass.new(relationship_name, options)
774
783
 
775
784
  associated_records_method_name = case relationship
776
- when JSONAPI::Relationship::ToOne then "record_for_#{attr}"
777
- when JSONAPI::Relationship::ToMany then "records_for_#{attr}"
785
+ when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}"
786
+ when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}"
778
787
  end
779
788
 
780
789
  foreign_key = relationship.foreign_key
@@ -784,6 +793,7 @@ module JSONAPI
784
793
  end unless method_defined?("#{foreign_key}=")
785
794
 
786
795
  define_method associated_records_method_name do
796
+ relationship = self.class._relationships[relationship_name]
787
797
  relation_name = relationship.relation_name(context: @context)
788
798
  records_for(relation_name)
789
799
  end unless method_defined?(associated_records_method_name)
@@ -794,10 +804,12 @@ module JSONAPI
794
804
  @model.method(foreign_key).call
795
805
  end unless method_defined?(foreign_key)
796
806
 
797
- define_method attr do |options = {}|
807
+ define_method relationship_name do |options = {}|
808
+ relationship = self.class._relationships[relationship_name]
809
+
798
810
  if relationship.polymorphic?
799
811
  associated_model = public_send(associated_records_method_name)
800
- resource_klass = self.class.resource_for_model_path(associated_model, self.class.module_path) if associated_model
812
+ resource_klass = self.class.resource_for_model(associated_model) if associated_model
801
813
  return resource_klass.new(associated_model, @context) if resource_klass
802
814
  else
803
815
  resource_klass = relationship.resource_klass
@@ -806,21 +818,25 @@ module JSONAPI
806
818
  return associated_model ? resource_klass.new(associated_model, @context) : nil
807
819
  end
808
820
  end
809
- end unless method_defined?(attr)
821
+ end unless method_defined?(relationship_name)
810
822
  else
811
823
  define_method foreign_key do
824
+ relationship = self.class._relationships[relationship_name]
825
+
812
826
  record = public_send(associated_records_method_name)
813
827
  return nil if record.nil?
814
828
  record.public_send(relationship.resource_klass._primary_key)
815
829
  end unless method_defined?(foreign_key)
816
830
 
817
- define_method attr do |options = {}|
831
+ define_method relationship_name do |options = {}|
832
+ relationship = self.class._relationships[relationship_name]
833
+
818
834
  resource_klass = relationship.resource_klass
819
835
  if resource_klass
820
836
  associated_model = public_send(associated_records_method_name)
821
837
  return associated_model ? resource_klass.new(associated_model, @context) : nil
822
838
  end
823
- end unless method_defined?(attr)
839
+ end unless method_defined?(relationship_name)
824
840
  end
825
841
  elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
826
842
  define_method foreign_key do
@@ -829,7 +845,10 @@ module JSONAPI
829
845
  record.public_send(relationship.resource_klass._primary_key)
830
846
  end
831
847
  end unless method_defined?(foreign_key)
832
- define_method attr do |options = {}|
848
+
849
+ define_method relationship_name do |options = {}|
850
+ relationship = self.class._relationships[relationship_name]
851
+
833
852
  resource_klass = relationship.resource_klass
834
853
  records = public_send(associated_records_method_name)
835
854
 
@@ -841,7 +860,7 @@ module JSONAPI
841
860
  sort_criteria = options.fetch(:sort_criteria, {})
842
861
  unless sort_criteria.nil? || sort_criteria.empty?
843
862
  order_options = relationship.resource_klass.construct_order_options(sort_criteria)
844
- records = resource_klass.apply_sort(records, order_options)
863
+ records = resource_klass.apply_sort(records, order_options, @context)
845
864
  end
846
865
 
847
866
  paginator = options[:paginator]
@@ -850,13 +869,38 @@ module JSONAPI
850
869
  end
851
870
 
852
871
  return records.collect do |record|
853
- resource_klass = self.class.resource_for_model_path(record, self.class.module_path)
872
+ if relationship.polymorphic?
873
+ resource_klass = self.class.resource_for_model(record)
874
+ end
854
875
  resource_klass.new(record, @context)
855
876
  end
856
- end unless method_defined?(attr)
877
+ end unless method_defined?(relationship_name)
857
878
  end
858
879
  end
859
880
  end
881
+
882
+ private
883
+
884
+ def check_reserved_resource_name(type, name)
885
+ if [:ids, :types, :hrefs, :links].include?(type)
886
+ warn "[NAME COLLISION] `#{name}` is a reserved resource name."
887
+ return
888
+ end
889
+ end
890
+
891
+ def check_reserved_attribute_name(name)
892
+ # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
893
+ # an attribute method won't be created for it.
894
+ if [:type].include?(name.to_sym)
895
+ warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
896
+ end
897
+ end
898
+
899
+ def check_reserved_relationship_name(name)
900
+ if [:id, :ids, :type, :types].include?(name.to_sym)
901
+ warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
902
+ end
903
+ end
860
904
  end
861
905
  end
862
906
  end