jsonapi-resources 0.9.0 → 0.9.12

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.
@@ -52,6 +52,7 @@ module JSONAPI
52
52
  end
53
53
 
54
54
  def setup_get_related_resource_action(params)
55
+ resolve_singleton_id(params)
55
56
  initialize_source(params)
56
57
  parse_fields(params[:fields])
57
58
  parse_include_directives(params[:include])
@@ -63,6 +64,7 @@ module JSONAPI
63
64
  end
64
65
 
65
66
  def setup_get_related_resources_action(params)
67
+ resolve_singleton_id(params)
66
68
  initialize_source(params)
67
69
  parse_fields(params[:fields])
68
70
  parse_include_directives(params[:include])
@@ -74,13 +76,17 @@ module JSONAPI
74
76
  end
75
77
 
76
78
  def setup_show_action(params)
79
+ resolve_singleton_id(params)
77
80
  parse_fields(params[:fields])
78
81
  parse_include_directives(params[:include])
82
+ parse_filters(params[:filter])
83
+
79
84
  @id = params[:id]
80
85
  add_show_operation
81
86
  end
82
87
 
83
88
  def setup_show_relationship_action(params)
89
+ resolve_singleton_id(params)
84
90
  add_show_relationship_operation(params[:relationship], params.require(@resource_klass._as_parent_key))
85
91
  end
86
92
 
@@ -91,24 +97,29 @@ module JSONAPI
91
97
  end
92
98
 
93
99
  def setup_create_relationship_action(params)
100
+ resolve_singleton_id(params)
94
101
  parse_modify_relationship_action(params, :add)
95
102
  end
96
103
 
97
104
  def setup_update_relationship_action(params)
105
+ resolve_singleton_id(params)
98
106
  parse_modify_relationship_action(params, :update)
99
107
  end
100
108
 
101
109
  def setup_update_action(params)
110
+ resolve_singleton_id(params)
102
111
  parse_fields(params[:fields])
103
112
  parse_include_directives(params[:include])
104
113
  parse_replace_operation(params.require(:data), params[:id])
105
114
  end
106
115
 
107
116
  def setup_destroy_action(params)
117
+ resolve_singleton_id(params)
108
118
  parse_remove_operation(params)
109
119
  end
110
120
 
111
121
  def setup_destroy_relationship_action(params)
122
+ resolve_singleton_id(params)
112
123
  parse_modify_relationship_action(params, :remove)
113
124
  end
114
125
 
@@ -170,13 +181,11 @@ module JSONAPI
170
181
  end
171
182
  type_resource = Resource.resource_for(@resource_klass.module_path + underscored_type.to_s)
172
183
  rescue NameError
173
- @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
174
- rescue JSONAPI::Exceptions::InvalidResource => e
175
- @errors.concat(e.errors)
184
+ fail JSONAPI::Exceptions::InvalidResource.new(type)
176
185
  end
177
186
 
178
187
  if type_resource.nil?
179
- @errors.concat(JSONAPI::Exceptions::InvalidResource.new(type).errors)
188
+ fail JSONAPI::Exceptions::InvalidResource.new(type)
180
189
  else
181
190
  unless values.nil?
182
191
  valid_fields = type_resource.fields.collect { |key| format_key(key) }
@@ -184,11 +193,11 @@ module JSONAPI
184
193
  if valid_fields.include?(field)
185
194
  extracted_fields[type].push unformat_key(field)
186
195
  else
187
- @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, field).errors)
196
+ fail JSONAPI::Exceptions::InvalidField.new(type, field)
188
197
  end
189
198
  end
190
199
  else
191
- @errors.concat(JSONAPI::Exceptions::InvalidField.new(type, 'nil').errors)
200
+ fail JSONAPI::Exceptions::InvalidField.new(type, 'nil')
192
201
  end
193
202
  end
194
203
  end
@@ -205,8 +214,7 @@ module JSONAPI
205
214
  check_include(Resource.resource_for(resource_klass.module_path + relationship.class_name.to_s.underscore), include_parts.last.partition('.'))
206
215
  end
207
216
  else
208
- @errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
209
- include_parts.first).errors)
217
+ fail JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type), include_parts.first)
210
218
  end
211
219
  end
212
220
 
@@ -214,31 +222,36 @@ module JSONAPI
214
222
  return unless raw_include
215
223
 
216
224
  unless JSONAPI.configuration.allow_include
217
- fail JSONAPI::Exceptions::ParametersNotAllowed.new([:include])
225
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(:include)
218
226
  end
219
227
 
220
228
  included_resources = []
221
229
  begin
222
- included_resources += CSV.parse_line(raw_include)
230
+ included_resources += Array(CSV.parse_line(raw_include))
223
231
  rescue CSV::MalformedCSVError
224
232
  fail JSONAPI::Exceptions::InvalidInclude.new(format_key(@resource_klass._type), raw_include)
225
233
  end
226
234
 
227
235
  return if included_resources.empty?
228
236
 
229
- result = included_resources.compact.map do |included_resource|
230
- check_include(@resource_klass, included_resource.partition('.'))
231
- unformat_key(included_resource).to_s
232
- end
237
+ begin
238
+ result = included_resources.compact.map do |included_resource|
239
+ check_include(@resource_klass, included_resource.partition('.'))
240
+ unformat_key(included_resource).to_s
241
+ end
233
242
 
234
- @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, result)
243
+ @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, result)
244
+ rescue JSONAPI::Exceptions::InvalidInclude => e
245
+ @errors.concat(e.errors)
246
+ @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, [])
247
+ end
235
248
  end
236
249
 
237
250
  def parse_filters(filters)
238
251
  return unless filters
239
252
 
240
253
  unless JSONAPI.configuration.allow_filter
241
- fail JSONAPI::Exceptions::ParametersNotAllowed.new([:filter])
254
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(:filter)
242
255
  end
243
256
 
244
257
  unless filters.class.method_defined?(:each)
@@ -247,12 +260,41 @@ module JSONAPI
247
260
  end
248
261
 
249
262
  filters.each do |key, value|
250
- filter = unformat_key(key)
251
- if @resource_klass._allowed_filter?(filter)
252
- @filters[filter] = value
263
+
264
+ unformatted_key = unformat_key(key)
265
+ if resource_klass._allowed_filter?(unformatted_key)
266
+ @filters[unformatted_key] = value
267
+ elsif unformatted_key.to_s.include?('.')
268
+ parse_relationship_filter(unformatted_key, value)
253
269
  else
254
- @errors.concat(JSONAPI::Exceptions::FilterNotAllowed.new(filter).errors)
270
+ return @errors.concat(Exceptions::FilterNotAllowed.new(unformatted_key).errors)
271
+ end
272
+ end
273
+ end
274
+
275
+ def parse_relationship_filter(key, value)
276
+ included_resource_name, filter_method = key.to_s.split('.')
277
+ filter_method = filter_method.to_sym if filter_method.present?
278
+
279
+ if included_resource_name
280
+ relationship = resource_klass._relationship(included_resource_name || '')
281
+
282
+ unless relationship
283
+ return @errors.concat(Exceptions::FilterNotAllowed.new(filter_method).errors)
284
+ end
285
+
286
+ unless relationship.resource_klass._allowed_filter?(filter_method)
287
+ return @errors.concat(Exceptions::FilterNotAllowed.new(filter_method).errors)
288
+ end
289
+
290
+ unless @include_directives.try(:include_config, relationship.name.to_sym).present?
291
+ return @errors.concat(Exceptions::FilterNotAllowed.new(filter_method).errors)
255
292
  end
293
+
294
+ verified_filter = relationship.resource_klass.verify_filters({ filter_method => value }, @context)
295
+ @include_directives.merge_filter(relationship.name, verified_filter)
296
+ else
297
+ return @errors.concat(Exceptions::FilterNotAllowed.new(filter_method).errors)
256
298
  end
257
299
  end
258
300
 
@@ -267,12 +309,12 @@ module JSONAPI
267
309
  return unless sort_criteria.present?
268
310
 
269
311
  unless JSONAPI.configuration.allow_sort
270
- fail JSONAPI::Exceptions::ParametersNotAllowed.new([:sort])
312
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(:sort)
271
313
  end
272
314
 
273
315
  sorts = []
274
316
  begin
275
- raw = URI.unescape(sort_criteria)
317
+ raw = URI::DEFAULT_PARSER.unescape(sort_criteria)
276
318
  sorts += CSV.parse_line(raw)
277
319
  rescue CSV::MalformedCSVError
278
320
  fail JSONAPI::Exceptions::InvalidSortCriteria.new(format_key(@resource_klass._type), raw)
@@ -296,9 +338,8 @@ module JSONAPI
296
338
  sort_field = sort_criteria[:field]
297
339
  sortable_fields = resource_klass.sortable_fields(context)
298
340
 
299
- unless sortable_fields.include? sort_field.to_sym
300
- @errors.concat(JSONAPI::Exceptions::InvalidSortCriteria
301
- .new(format_key(resource_klass._type), sort_field).errors)
341
+ unless sortable_fields.include?sort_field.to_sym
342
+ fail JSONAPI::Exceptions::InvalidSortCriteria.new(format_key(resource_klass._type), sort_field)
302
343
  end
303
344
  end
304
345
 
@@ -352,7 +393,7 @@ module JSONAPI
352
393
  relationship_type: relationship_type,
353
394
  source_klass: @source_klass,
354
395
  source_id: @source_id,
355
- filters: @source_klass.verify_filters(@filters, @context),
396
+ filters: @filters,
356
397
  sort_criteria: @sort_criteria,
357
398
  paginator: @paginator,
358
399
  fields: @fields,
@@ -473,7 +514,7 @@ module JSONAPI
473
514
 
474
515
  unless links_object[:id].nil?
475
516
  resource = self.resource_klass || Resource
476
- relationship_resource = resource.resource_for(unformat_key(links_object[:type]).to_s)
517
+ relationship_resource = resource.resource_for(unformat_key(relationship.options[:class_name] || links_object[:type]).to_s)
477
518
  relationship_id = relationship_resource.verify_key(links_object[:id], @context)
478
519
  if relationship.polymorphic?
479
520
  { id: relationship_id, type: unformat_key(links_object[:type].to_s) }
@@ -496,20 +537,40 @@ module JSONAPI
496
537
 
497
538
  links_object = parse_to_many_links_object(linkage)
498
539
 
499
- # Since we do not yet support polymorphic to_many relationships we will raise an error if the type does not match the
500
- # relationship's type.
501
- # ToDo: Support Polymorphic relationships
502
-
503
540
  if links_object.length == 0
504
541
  add_result.call([])
505
542
  else
506
- if links_object.length > 1 || !links_object.has_key?(unformat_key(relationship.type).to_s)
507
- fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
508
- end
543
+ if relationship.polymorphic?
544
+ polymorphic_results = []
545
+
546
+ links_object.each_pair do |type, keys|
547
+ resource = self.resource_klass || Resource
548
+ type_name = unformat_key(type).to_s
549
+
550
+ relationship_resource_klass = resource.resource_for(relationship.class_name)
551
+ relationship_klass = relationship_resource_klass._model_class
552
+
553
+ linkage_object_resource_klass = resource.resource_for(type_name)
554
+ linkage_object_klass = linkage_object_resource_klass._model_class
555
+
556
+ unless linkage_object_klass == relationship_klass || linkage_object_klass.in?(relationship_klass.subclasses)
557
+ fail JSONAPI::Exceptions::TypeMismatch.new(type_name)
558
+ end
509
559
 
510
- links_object.each_pair do |type, keys|
511
- relationship_resource = Resource.resource_for(@resource_klass.module_path + unformat_key(type).to_s)
512
- add_result.call relationship_resource.verify_keys(keys, @context)
560
+ relationship_ids = relationship_resource_klass.verify_keys(keys, @context)
561
+ polymorphic_results << { type: type, ids: relationship_ids }
562
+ end
563
+
564
+ add_result.call polymorphic_results
565
+ else
566
+ relationship_type = unformat_key(relationship.type).to_s
567
+
568
+ if links_object.length > 1 || !links_object.has_key?(relationship_type)
569
+ fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
570
+ end
571
+
572
+ relationship_resource = Resource.resource_for(@resource_klass.module_path + relationship_type)
573
+ add_result.call relationship_resource.verify_keys(links_object[relationship_type], @context)
513
574
  end
514
575
  end
515
576
  end
@@ -528,8 +589,10 @@ module JSONAPI
528
589
  when 'relationships'
529
590
  value.keys.each do |links_key|
530
591
  unless formatted_allowed_fields.include?(links_key.to_sym)
531
- params_not_allowed.push(links_key)
532
- unless JSONAPI.configuration.raise_if_parameters_not_allowed
592
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
593
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(links_key)
594
+ else
595
+ params_not_allowed.push(links_key)
533
596
  value.delete links_key
534
597
  end
535
598
  end
@@ -537,8 +600,10 @@ module JSONAPI
537
600
  when 'attributes'
538
601
  value.each do |attr_key, attr_value|
539
602
  unless formatted_allowed_fields.include?(attr_key.to_sym)
540
- params_not_allowed.push(attr_key)
541
- unless JSONAPI.configuration.raise_if_parameters_not_allowed
603
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
604
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(attr_key)
605
+ else
606
+ params_not_allowed.push(attr_key)
542
607
  value.delete attr_key
543
608
  end
544
609
  end
@@ -546,27 +611,30 @@ module JSONAPI
546
611
  when 'type'
547
612
  when 'id'
548
613
  unless formatted_allowed_fields.include?(:id)
549
- params_not_allowed.push(:id)
550
- unless JSONAPI.configuration.raise_if_parameters_not_allowed
614
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
615
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(:id)
616
+ else
617
+ params_not_allowed.push(:id)
551
618
  params.delete :id
552
619
  end
553
620
  end
554
621
  else
555
- params_not_allowed.push(key)
622
+ if JSONAPI.configuration.raise_if_parameters_not_allowed
623
+ fail JSONAPI::Exceptions::ParameterNotAllowed.new(key)
624
+ else
625
+ params_not_allowed.push(key)
626
+ params.delete key
627
+ end
556
628
  end
557
629
  end
558
630
 
559
631
  if params_not_allowed.length > 0
560
- if JSONAPI.configuration.raise_if_parameters_not_allowed
561
- fail JSONAPI::Exceptions::ParametersNotAllowed.new(params_not_allowed)
562
- else
563
- params_not_allowed_warnings = params_not_allowed.map do |key|
564
- JSONAPI::Warning.new(code: JSONAPI::PARAM_NOT_ALLOWED,
565
- title: 'Param not allowed',
566
- detail: "#{key} is not allowed.")
567
- end
568
- self.warnings.concat(params_not_allowed_warnings)
632
+ params_not_allowed_warnings = params_not_allowed.map do |param|
633
+ JSONAPI::Warning.new(code: JSONAPI::PARAM_NOT_ALLOWED,
634
+ title: 'Param not allowed',
635
+ detail: "#{param} is not allowed.")
569
636
  end
637
+ self.warnings.concat(params_not_allowed_warnings)
570
638
  end
571
639
  end
572
640
 
@@ -635,7 +703,8 @@ module JSONAPI
635
703
  end
636
704
 
637
705
  def parse_replace_operation(data, keys)
638
- parse_single_replace_operation(data, [keys], id_key_presence_check_required: keys.present?)
706
+ parse_single_replace_operation(data, [keys],
707
+ id_key_presence_check_required: keys.present? && !@resource_klass.singleton?)
639
708
  rescue JSONAPI::Exceptions::Error => e
640
709
  @errors.concat(e.errors)
641
710
  end
@@ -666,6 +735,13 @@ module JSONAPI
666
735
  end
667
736
  end
668
737
 
738
+ def resolve_singleton_id(params)
739
+ if @resource_klass.singleton? && params[:id].nil?
740
+ key = @resource_klass.singleton_key(context)
741
+ params[:id] = key
742
+ end
743
+ end
744
+
669
745
  def format_key(key)
670
746
  @key_formatter.format(key)
671
747
  end
@@ -5,6 +5,9 @@ module JSONAPI
5
5
  class Resource
6
6
  include Callbacks
7
7
 
8
+ DEFAULT_ATTRIBUTE_OPTIONS = { format: :default }.freeze
9
+ MODULE_PATH_REGEXP = /::[^:]+\Z/.freeze
10
+
8
11
  attr_reader :context
9
12
 
10
13
  define_jsonapi_resources_callbacks :create,
@@ -302,6 +305,26 @@ module JSONAPI
302
305
  to_add = relationship_key_values - (relationship_key_values & existing)
303
306
  _create_to_many_links(relationship_type, to_add, {})
304
307
 
308
+ @reload_needed = true
309
+ elsif relationship.polymorphic?
310
+ relationship_key_values.each do |relationship_key_value|
311
+ relationship_resource_klass = self.class.resource_for(relationship_key_value[:type])
312
+ ids = relationship_key_value[:ids]
313
+
314
+ related_records = relationship_resource_klass
315
+ .records(options)
316
+ .where({relationship_resource_klass._primary_key => ids})
317
+
318
+ missed_ids = ids - related_records.pluck(relationship_resource_klass._primary_key)
319
+
320
+ if missed_ids.present?
321
+ fail JSONAPI::Exceptions::RecordNotFound.new(missed_ids)
322
+ end
323
+
324
+ relation_name = relationship.relation_name(context: @context)
325
+ @model.send("#{relation_name}") << related_records
326
+ end
327
+
305
328
  @reload_needed = true
306
329
  else
307
330
  send("#{relationship.foreign_key}=", relationship_key_values)
@@ -415,6 +438,8 @@ module JSONAPI
415
438
  subclass.abstract(false)
416
439
  subclass.immutable(false)
417
440
  subclass.caching(false)
441
+ subclass.singleton(singleton?, (_singleton_options.dup || {}))
442
+ subclass.exclude_links(_exclude_links)
418
443
  subclass._attributes = (_attributes || {}).dup
419
444
 
420
445
  subclass._model_hints = (_model_hints || {}).dup
@@ -435,6 +460,9 @@ module JSONAPI
435
460
  end
436
461
 
437
462
  check_reserved_resource_name(subclass._type, subclass.name)
463
+
464
+ subclass._routed = false
465
+ subclass._warned_missing_route = false
438
466
  end
439
467
 
440
468
  def rebuild_relationships(relationships)
@@ -453,7 +481,7 @@ module JSONAPI
453
481
 
454
482
  def resource_for(type)
455
483
  type = type.underscore
456
- type_with_module = type.include?('/') ? type : module_path + type
484
+ type_with_module = type.start_with?(module_path) ? type : module_path + type
457
485
 
458
486
  resource_name = _resource_name_from_type(type_with_module)
459
487
  resource = resource_name.safe_constantize if resource_name
@@ -480,7 +508,7 @@ module JSONAPI
480
508
  end
481
509
  end
482
510
 
483
- attr_accessor :_attributes, :_relationships, :_type, :_model_hints
511
+ attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route
484
512
  attr_writer :_allowed_filters, :_paginator
485
513
 
486
514
  def create(context)
@@ -507,10 +535,12 @@ module JSONAPI
507
535
  end
508
536
  end
509
537
 
510
- def attribute(attr, options = {})
538
+ def attribute(attribute_name, options = {})
539
+ attr = attribute_name.to_sym
540
+
511
541
  check_reserved_attribute_name(attr)
512
542
 
513
- if (attr.to_sym == :id) && (options[:format].nil?)
543
+ if (attr == :id) && (options[:format].nil?)
514
544
  ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
515
545
  end
516
546
 
@@ -528,7 +558,7 @@ module JSONAPI
528
558
  end
529
559
 
530
560
  def default_attribute_options
531
- { format: :default }
561
+ DEFAULT_ATTRIBUTE_OPTIONS
532
562
  end
533
563
 
534
564
  def relationship(*attrs)
@@ -563,7 +593,14 @@ module JSONAPI
563
593
  _add_relationship(Relationship::ToMany, *attrs)
564
594
  end
565
595
 
596
+ # @model_class is inherited from superclass, and this causes some issues:
597
+ # ```
598
+ # CarResource._model_class #=> Vehicle # it should be Car
599
+ # ```
600
+ # so in order to invoke the right class from subclasses,
601
+ # we should call this method to override it.
566
602
  def model_name(model, options = {})
603
+ @model_class = nil
567
604
  @_model_name = model.to_sym
568
605
 
569
606
  model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
@@ -577,6 +614,19 @@ module JSONAPI
577
614
  _model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
578
615
  end
579
616
 
617
+ def singleton(*attrs)
618
+ @_singleton = (!!attrs[0] == attrs[0]) ? attrs[0] : true
619
+ @_singleton_options = attrs.extract_options!
620
+ end
621
+
622
+ def _singleton_options
623
+ @_singleton_options ||= {}
624
+ end
625
+
626
+ def singleton?
627
+ @_singleton ||= false
628
+ end
629
+
580
630
  def filters(*attrs)
581
631
  @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
582
632
  end
@@ -636,7 +686,7 @@ module JSONAPI
636
686
  include_directives = options[:include_directives]
637
687
  if include_directives
638
688
  model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
639
- records = records.includes(model_includes)
689
+ records = records.includes(model_includes) if model_includes.present?
640
690
  end
641
691
 
642
692
  records
@@ -730,9 +780,30 @@ module JSONAPI
730
780
  records
731
781
  end
732
782
 
783
+ def apply_included_resources_filters(records, options = {})
784
+ include_directives = options[:include_directives]
785
+ return records unless include_directives
786
+ related_directives = include_directives.include_directives.fetch(:include_related)
787
+ related_directives.reduce(records) do |memo, (relationship_name, config)|
788
+ relationship = _relationship(relationship_name)
789
+ next memo unless relationship && relationship.is_a?(JSONAPI::Relationship::ToMany)
790
+ filtering_resource = relationship.resource_klass
791
+
792
+ # Don't try to merge where clauses when relation isn't already being joined to query.
793
+ next memo unless config[:include_in_join]
794
+
795
+ filters = config[:include_filters]
796
+ next memo unless filters
797
+
798
+ rel_records = filtering_resource.apply_filters(filtering_resource.records(options), filters, options).references(relationship_name)
799
+ memo.merge(rel_records)
800
+ end
801
+ end
802
+
733
803
  def filter_records(filters, options, records = records(options))
734
804
  records = apply_filters(records, filters, options)
735
- apply_includes(records, options)
805
+ records = apply_includes(records, options)
806
+ apply_included_resources_filters(records, options)
736
807
  end
737
808
 
738
809
  def sort_records(records, order_options, context = {})
@@ -808,6 +879,9 @@ module JSONAPI
808
879
 
809
880
  def verify_filters(filters, context = nil)
810
881
  verified_filters = {}
882
+
883
+ return verified_filters if filters.nil?
884
+
811
885
  filters.each do |filter, raw_value|
812
886
  verified_filter = verify_filter(filter, raw_value, context)
813
887
  verified_filters[verified_filter[0]] = verified_filter[1]
@@ -855,6 +929,24 @@ module JSONAPI
855
929
  @_resource_key_type ||= JSONAPI.configuration.resource_key_type
856
930
  end
857
931
 
932
+ # override to all resolution of masked ids to actual ids. Because singleton routes do not specify the id this
933
+ # will be needed to allow lookup of singleton resources. Alternately singleton resources can override
934
+ # `verify_key`
935
+ def singleton_key(context)
936
+ if @_singleton_options && @_singleton_options[:singleton_key]
937
+ strategy = @_singleton_options[:singleton_key]
938
+ case strategy
939
+ when Proc
940
+ key = strategy.call(context)
941
+ when Symbol, String
942
+ key = send(strategy, context)
943
+ else
944
+ raise "singleton_key must be a proc or function name"
945
+ end
946
+ end
947
+ key
948
+ end
949
+
858
950
  def verify_key(key, context = nil)
859
951
  key_type = resource_key_type
860
952
 
@@ -906,6 +998,10 @@ module JSONAPI
906
998
  default_attribute_options.merge(@_attributes[attr])
907
999
  end
908
1000
 
1001
+ def _has_attribute?(attr)
1002
+ @_attributes.keys.include?(attr.to_sym)
1003
+ end
1004
+
909
1005
  def _updatable_relationships
910
1006
  @_relationships.map { |key, _relationship| key }
911
1007
  end
@@ -975,6 +1071,31 @@ module JSONAPI
975
1071
  !@immutable
976
1072
  end
977
1073
 
1074
+ def exclude_links(exclude)
1075
+ _resolve_exclude_links(exclude)
1076
+ end
1077
+
1078
+ def _exclude_links
1079
+ @_exclude_links ||= _resolve_exclude_links(JSONAPI.configuration.default_exclude_links)
1080
+ end
1081
+
1082
+ def exclude_link?(link)
1083
+ _exclude_links.include?(link.to_sym)
1084
+ end
1085
+
1086
+ def _resolve_exclude_links(exclude)
1087
+ case exclude
1088
+ when :default, "default"
1089
+ @_exclude_links = [:self]
1090
+ when :none, "none"
1091
+ @_exclude_links = []
1092
+ when Array
1093
+ @_exclude_links = exclude.collect {|link| link.to_sym}
1094
+ else
1095
+ fail "Invalid exclude_links"
1096
+ end
1097
+ end
1098
+
978
1099
  def caching(val = true)
979
1100
  @caching = val
980
1101
  end
@@ -1015,7 +1136,7 @@ module JSONAPI
1015
1136
  if name == 'JSONAPI::Resource'
1016
1137
  ''
1017
1138
  else
1018
- name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
1139
+ name =~ MODULE_PATH_REGEXP ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
1019
1140
  end
1020
1141
  end
1021
1142
 
@@ -1038,7 +1159,8 @@ module JSONAPI
1038
1159
  options = attrs.extract_options!
1039
1160
  options[:parent_resource] = self
1040
1161
 
1041
- attrs.each do |relationship_name|
1162
+ attrs.each do |name|
1163
+ relationship_name = name.to_sym
1042
1164
  check_reserved_relationship_name(relationship_name)
1043
1165
  check_duplicate_relationship_name(relationship_name)
1044
1166
 
@@ -1232,7 +1354,8 @@ module JSONAPI
1232
1354
  rel_id = row[index+1]
1233
1355
  assoc_rels = res.preloaded_fragments[rel_name]
1234
1356
  if index == path.length - 1
1235
- assoc_rels[rel_id] = target_resources[klass.name].fetch(rel_id)
1357
+ association_res = target_resources[klass.name].fetch(rel_id, nil)
1358
+ assoc_rels[rel_id] = association_res if association_res
1236
1359
  else
1237
1360
  res = assoc_rels[rel_id]
1238
1361
  end
@@ -1246,7 +1369,7 @@ module JSONAPI
1246
1369
  quoted_attrs = attrs.map do |attr|
1247
1370
  quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name)
1248
1371
  quoted_column = conn.quote_column_name(attr.name)
1249
- "#{quoted_table}.#{quoted_column}"
1372
+ Arel.sql("#{quoted_table}.#{quoted_column}")
1250
1373
  end
1251
1374
  relation.pluck(*quoted_attrs)
1252
1375
  end
@@ -5,10 +5,10 @@ module JSONAPI
5
5
  ActionController::Rendering,
6
6
  ActionController::Renderers::All,
7
7
  ActionController::StrongParameters,
8
- ActionController::ForceSSL,
8
+ Gem::Requirement.new('< 6.1').satisfied_by?(ActionPack.gem_version) ? ActionController::ForceSSL : nil,
9
9
  ActionController::Instrumentation,
10
10
  JSONAPI::ActsAsResourceController
11
- ].freeze
11
+ ].compact.freeze
12
12
 
13
13
  MODULES.each do |mod|
14
14
  include mod