jsonapi-resources 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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