jsonapi-resources 0.8.3 → 0.9.0.beta1

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.
@@ -7,7 +7,6 @@ module JSONAPI
7
7
  PARAM_NOT_ALLOWED = '105'
8
8
  PARAM_MISSING = '106'
9
9
  INVALID_FILTER_VALUE = '107'
10
- COUNT_MISMATCH = '108'
11
10
  KEY_ORDER_MISMATCH = '109'
12
11
  KEY_NOT_INCLUDED_IN_URL = '110'
13
12
  INVALID_INCLUDE = '112'
@@ -20,6 +19,7 @@ module JSONAPI
20
19
  INVALID_FIELD_FORMAT = '119'
21
20
  INVALID_FILTERS_SYNTAX = '120'
22
21
  SAVE_FAILED = '121'
22
+ INVALID_DATA_FORMAT = '122'
23
23
  FORBIDDEN = '403'
24
24
  RECORD_NOT_FOUND = '404'
25
25
  NOT_ACCEPTABLE = '406'
@@ -36,7 +36,6 @@ module JSONAPI
36
36
  PARAM_NOT_ALLOWED => 'PARAM_NOT_ALLOWED',
37
37
  PARAM_MISSING => 'PARAM_MISSING',
38
38
  INVALID_FILTER_VALUE => 'INVALID_FILTER_VALUE',
39
- COUNT_MISMATCH => 'COUNT_MISMATCH',
40
39
  KEY_ORDER_MISMATCH => 'KEY_ORDER_MISMATCH',
41
40
  KEY_NOT_INCLUDED_IN_URL => 'KEY_NOT_INCLUDED_IN_URL',
42
41
  INVALID_INCLUDE => 'INVALID_INCLUDE',
@@ -49,6 +48,7 @@ module JSONAPI
49
48
  INVALID_FIELD_FORMAT => 'INVALID_FIELD_FORMAT',
50
49
  INVALID_FILTERS_SYNTAX => 'INVALID_FILTERS_SYNTAX',
51
50
  SAVE_FAILED => 'SAVE_FAILED',
51
+ INVALID_DATA_FORMAT => 'INVALID_DATA_FORMAT',
52
52
  FORBIDDEN => 'FORBIDDEN',
53
53
  RECORD_NOT_FOUND => 'RECORD_NOT_FOUND',
54
54
  NOT_ACCEPTABLE => 'NOT_ACCEPTABLE',
@@ -1,6 +1,12 @@
1
1
  module JSONAPI
2
2
  module Exceptions
3
- class Error < RuntimeError; end
3
+ class Error < RuntimeError
4
+ def errors
5
+ # :nocov:
6
+ raise NotImplementedError, "Subclass of Error must implement errors method"
7
+ # :nocov:
8
+ end
9
+ end
4
10
 
5
11
  class InternalServerError < Error
6
12
  attr_accessor :exception
@@ -10,7 +16,7 @@ module JSONAPI
10
16
  end
11
17
 
12
18
  def errors
13
- unless Rails.env.production?
19
+ if JSONAPI.configuration.include_backtraces_in_errors
14
20
  meta = Hash.new
15
21
  meta[:exception] = exception.message
16
22
  meta[:backtrace] = exception.backtrace
@@ -204,6 +210,17 @@ module JSONAPI
204
210
  end
205
211
  end
206
212
 
213
+ class InvalidDataFormat < Error
214
+ def errors
215
+ [JSONAPI::Error.new(code: JSONAPI::INVALID_DATA_FORMAT,
216
+ status: :bad_request,
217
+ title: I18n.translate('jsonapi-resources.exceptions.invalid_data_format.title',
218
+ default: 'Invalid data format'),
219
+ detail: I18n.translate('jsonapi-resources.exceptions.invalid_data_format.detail',
220
+ default: 'Data must be a hash.'))]
221
+ end
222
+ end
223
+
207
224
  class InvalidLinksObject < Error
208
225
  def errors
209
226
  [JSONAPI::Error.new(code: JSONAPI::INVALID_LINKS_OBJECT,
@@ -320,17 +337,6 @@ module JSONAPI
320
337
  end
321
338
  end
322
339
 
323
- class CountMismatch < Error
324
- def errors
325
- [JSONAPI::Error.new(code: JSONAPI::COUNT_MISMATCH,
326
- status: :bad_request,
327
- title: I18n.translate('jsonapi-resources.exceptions.count_mismatch.title',
328
- default: 'Count to key mismatch'),
329
- detail: I18n.translate('jsonapi-resources.exceptions.count_mismatch.detail',
330
- default: 'The resource collection does not contain the same number of objects as the number of keys.'))]
331
- end
332
- end
333
-
334
340
  class KeyNotIncludedInURL < Error
335
341
  attr_accessor :key
336
342
  def initialize(key)
@@ -13,6 +13,10 @@ module JSONAPI
13
13
  return FormatterWrapperCache.new(self)
14
14
  end
15
15
 
16
+ def uncached
17
+ return self
18
+ end
19
+
16
20
  def formatter_for(format)
17
21
  "#{format.to_s.camelize}Formatter".safe_constantize
18
22
  end
@@ -80,6 +84,10 @@ module JSONAPI
80
84
  def cached
81
85
  self
82
86
  end
87
+
88
+ def uncached
89
+ return @formatter_klass
90
+ end
83
91
  end
84
92
  end
85
93
 
@@ -113,7 +121,13 @@ end
113
121
  class DefaultValueFormatter < JSONAPI::ValueFormatter
114
122
  class << self
115
123
  def format(raw_value)
116
- raw_value
124
+ case raw_value
125
+ when Date, Time, DateTime, ActiveSupport::TimeWithZone, BigDecimal
126
+ # Use the as_json methods added to various base classes by ActiveSupport
127
+ return raw_value.as_json
128
+ else
129
+ return raw_value
130
+ end
117
131
  end
118
132
  end
119
133
  end
@@ -36,6 +36,10 @@ module JSONAPI
36
36
  get_includes(@include_directives_hash)
37
37
  end
38
38
 
39
+ def paths
40
+ delve_paths(get_includes(@include_directives_hash, false))
41
+ end
42
+
39
43
  private
40
44
 
41
45
  def get_related(current_path)
@@ -59,9 +63,12 @@ module JSONAPI
59
63
  current
60
64
  end
61
65
 
62
- def get_includes(directive)
63
- directive[:include_related].select { |k,v| v[:include_in_join] }.map do |name, directive|
64
- sub = get_includes(directive)
66
+ def get_includes(directive, only_joined_includes = true)
67
+ ir = directive[:include_related]
68
+ ir = ir.select { |k,v| v[:include_in_join] } if only_joined_includes
69
+
70
+ ir.map do |name, sub_directive|
71
+ sub = get_includes(sub_directive, only_joined_includes)
65
72
  sub.any? ? { name => sub } : name
66
73
  end
67
74
  end
@@ -76,5 +83,18 @@ module JSONAPI
76
83
  related[:include] = true
77
84
  end
78
85
  end
86
+
87
+ def delve_paths(obj)
88
+ case obj
89
+ when Array
90
+ obj.map{|elem| delve_paths(elem)}.flatten(1)
91
+ when Hash
92
+ obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
93
+ when Symbol, String
94
+ [[obj]]
95
+ else
96
+ raise "delve_paths cannot descend into #{obj.class.name}"
97
+ end
98
+ end
79
99
  end
80
100
  end
@@ -11,9 +11,9 @@ module JSONAPI
11
11
  :replace_fields,
12
12
  :replace_to_one_relationship,
13
13
  :replace_polymorphic_to_one_relationship,
14
- :create_to_many_relationship,
15
- :replace_to_many_relationship,
16
- :remove_to_many_relationship,
14
+ :create_to_many_relationships,
15
+ :replace_to_many_relationships,
16
+ :remove_to_many_relationships,
17
17
  :remove_to_one_relationship,
18
18
  :operation
19
19
 
@@ -71,12 +71,21 @@ module JSONAPI
71
71
  fields = params[:fields]
72
72
 
73
73
  verified_filters = resource_klass.verify_filters(filters, context)
74
- resource_records = resource_klass.find(verified_filters,
75
- context: context,
76
- include_directives: include_directives,
77
- sort_criteria: sort_criteria,
78
- paginator: paginator,
79
- fields: fields)
74
+ find_options = {
75
+ context: context,
76
+ include_directives: include_directives,
77
+ sort_criteria: sort_criteria,
78
+ paginator: paginator,
79
+ fields: fields
80
+ }
81
+
82
+ resource_records = if params[:cache_serializer]
83
+ resource_klass.find_serialized_with_caching(verified_filters,
84
+ params[:cache_serializer],
85
+ find_options)
86
+ else
87
+ resource_klass.find(verified_filters, find_options)
88
+ end
80
89
 
81
90
  page_options = {}
82
91
  if (JSONAPI.configuration.top_level_meta_include_record_count ||
@@ -104,10 +113,19 @@ module JSONAPI
104
113
 
105
114
  key = resource_klass.verify_key(id, context)
106
115
 
107
- resource_record = resource_klass.find_by_key(key,
108
- context: context,
109
- include_directives: include_directives,
110
- fields: fields)
116
+ find_options = {
117
+ context: context,
118
+ include_directives: include_directives,
119
+ fields: fields
120
+ }
121
+
122
+ resource_record = if params[:cache_serializer]
123
+ resource_klass.find_by_key_serialized_with_caching(key,
124
+ params[:cache_serializer],
125
+ find_options)
126
+ else
127
+ resource_klass.find_by_key(key, find_options)
128
+ end
111
129
 
112
130
  return JSONAPI::ResourceOperationResult.new(:ok, resource_record)
113
131
  end
@@ -129,6 +147,7 @@ module JSONAPI
129
147
  relationship_type = params[:relationship_type].to_sym
130
148
  fields = params[:fields]
131
149
 
150
+ # TODO Should fetch related_resource from cache if caching enabled
132
151
  source_resource = source_klass.find_by_key(source_id, context: context, fields: fields)
133
152
 
134
153
  related_resource = source_resource.public_send(relationship_type)
@@ -141,20 +160,37 @@ module JSONAPI
141
160
  source_id = params[:source_id]
142
161
  relationship_type = params[:relationship_type]
143
162
  filters = params[:filters]
144
- include_directives = params[:include_directives]
145
163
  sort_criteria = params[:sort_criteria]
146
164
  paginator = params[:paginator]
147
165
  fields = params[:fields]
166
+ include_directives = params[:include_directives]
148
167
 
149
168
  source_resource ||= source_klass.find_by_key(source_id, context: context, fields: fields)
150
169
 
151
- related_resources = source_resource.public_send(relationship_type,
152
- context: context,
153
- filters: filters,
154
- include_directives: include_directives,
155
- sort_criteria: sort_criteria,
156
- paginator: paginator,
157
- fields: fields)
170
+ rel_opts = {
171
+ filters: filters,
172
+ sort_criteria: sort_criteria,
173
+ paginator: paginator,
174
+ fields: fields,
175
+ context: context,
176
+ include_directives: include_directives
177
+ }
178
+
179
+ related_resources = nil
180
+ if params[:cache_serializer]
181
+ # TODO Could also avoid instantiating source_resource as actual Resource by
182
+ # allowing LinkBuilder to accept CachedResourceFragment as source in
183
+ # relationships_related_link
184
+ scope = source_resource.public_send(:"records_for_#{relationship_type}", rel_opts)
185
+ relationship = source_klass._relationship(relationship_type)
186
+ related_resources = relationship.resource_klass.find_serialized_with_caching(
187
+ scope,
188
+ params[:cache_serializer],
189
+ rel_opts
190
+ )
191
+ else
192
+ related_resources = source_resource.public_send(relationship_type, rel_opts)
193
+ end
158
194
 
159
195
  if ((JSONAPI.configuration.top_level_meta_include_record_count) ||
160
196
  (paginator && paginator.class.requires_record_count) ||
@@ -240,7 +276,7 @@ module JSONAPI
240
276
  return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
241
277
  end
242
278
 
243
- def create_to_many_relationship
279
+ def create_to_many_relationships
244
280
  resource_id = params[:resource_id]
245
281
  relationship_type = params[:relationship_type].to_sym
246
282
  data = params[:data]
@@ -251,7 +287,7 @@ module JSONAPI
251
287
  return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
252
288
  end
253
289
 
254
- def replace_to_many_relationship
290
+ def replace_to_many_relationships
255
291
  resource_id = params[:resource_id]
256
292
  relationship_type = params[:relationship_type].to_sym
257
293
  data = params.fetch(:data)
@@ -262,15 +298,21 @@ module JSONAPI
262
298
  return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
263
299
  end
264
300
 
265
- def remove_to_many_relationship
301
+ def remove_to_many_relationships
266
302
  resource_id = params[:resource_id]
267
303
  relationship_type = params[:relationship_type].to_sym
268
- associated_key = params[:associated_key]
304
+ associated_keys = params[:associated_keys]
269
305
 
270
306
  resource = resource_klass.find_by_key(resource_id, context: context)
271
- result = resource.remove_to_many_link(relationship_type, associated_key)
272
307
 
273
- return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted)
308
+ complete = true
309
+ associated_keys.each do |key|
310
+ result = resource.remove_to_many_link(relationship_type, key)
311
+ if complete && result != :completed
312
+ complete = false
313
+ end
314
+ end
315
+ return JSONAPI::OperationResult.new(complete ? :no_content : :accepted)
274
316
  end
275
317
 
276
318
  def remove_to_one_relationship
@@ -53,10 +53,30 @@ module JSONAPI
53
53
  }
54
54
  .fetch(type)
55
55
 
56
- define_on_resource associated_records_method_name do
56
+ define_on_resource associated_records_method_name do |options = {}|
57
57
  relationship = self.class._relationships[relationship_name]
58
58
  relation_name = relationship.relation_name(context: @context)
59
- records_for(relation_name)
59
+ records = records_for(relation_name)
60
+
61
+ resource_klass = relationship.resource_klass
62
+
63
+ filters = options.fetch(:filters, {})
64
+ unless filters.nil? || filters.empty?
65
+ records = resource_klass.apply_filters(records, filters, options)
66
+ end
67
+
68
+ sort_criteria = options.fetch(:sort_criteria, {})
69
+ unless sort_criteria.nil? || sort_criteria.empty?
70
+ order_options = relationship.resource_klass.construct_order_options(sort_criteria)
71
+ records = resource_klass.apply_sort(records, order_options, @context)
72
+ end
73
+
74
+ paginator = options[:paginator]
75
+ if paginator
76
+ records = resource_klass.apply_pagination(records, paginator, order_options)
77
+ end
78
+
79
+ records
60
80
  end
61
81
 
62
82
  associated_records_method_name
@@ -122,25 +142,7 @@ module JSONAPI
122
142
  relationship = self.class._relationships[relationship_name]
123
143
 
124
144
  resource_klass = relationship.resource_klass
125
- records = public_send(associated_records_method_name)
126
-
127
- filters = options.fetch(:filters, {})
128
- unless filters.nil? || filters.empty?
129
- records = resource_klass.apply_filters(records, filters, options)
130
- end
131
-
132
- records = resource_klass.apply_includes(records, options)
133
-
134
- sort_criteria = options.fetch(:sort_criteria, {})
135
- unless sort_criteria.nil? || sort_criteria.empty?
136
- order_options = relationship.resource_klass.construct_order_options(sort_criteria)
137
- records = resource_klass.apply_sort(records, order_options, @context)
138
- end
139
-
140
- paginator = options[:paginator]
141
- if paginator
142
- records = resource_klass.apply_pagination(records, paginator, order_options)
143
- end
145
+ records = public_send(associated_records_method_name, options)
144
146
 
145
147
  return records.collect do |record|
146
148
  if relationship.polymorphic?
@@ -122,7 +122,7 @@ module JSONAPI
122
122
  if data_required
123
123
  data = params.fetch(:data)
124
124
  object_params = { relationships: { format_key(relationship.name) => { data: data } } }
125
- verified_params = parse_params(object_params, updatable_fields)
125
+ verified_params = parse_params(object_params, @resource_klass.updatable_fields(@context))
126
126
 
127
127
  parse_arguments = [verified_params, relationship, parent_key]
128
128
  else
@@ -201,7 +201,7 @@ module JSONAPI
201
201
  relationship = resource_klass._relationship(relationship_name)
202
202
  if relationship && format_key(relationship_name) == include_parts.first
203
203
  unless include_parts.last.empty?
204
- check_include(Resource.resource_for(resource_klass.module_path + relationship.class_name.to_s.underscore), include_parts.last.partition('.'))
204
+ check_include(Resource.resource_for(@resource_klass.module_path + relationship.class_name.to_s.underscore), include_parts.last.partition('.'))
205
205
  end
206
206
  else
207
207
  @errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
@@ -346,30 +346,19 @@ module JSONAPI
346
346
  )
347
347
  end
348
348
 
349
- # TODO: Please remove after `createable_fields` is removed
350
- # :nocov:
351
- def creatable_fields
352
- if @resource_klass.respond_to?(:createable_fields)
353
- creatable_fields = @resource_klass.createable_fields(@context)
354
- else
355
- creatable_fields = @resource_klass.creatable_fields(@context)
356
- end
357
- end
358
- # :nocov:
349
+ def parse_add_operation(params)
350
+ fail JSONAPI::Exceptions::InvalidDataFormat unless params.respond_to?(:each_pair)
359
351
 
360
- def parse_add_operation(data)
361
- Array.wrap(data).each do |params|
362
- verify_type(params[:type])
352
+ verify_type(params[:type])
363
353
 
364
- data = parse_params(params, creatable_fields)
365
- @operations.push JSONAPI::Operation.new(:create_resource,
366
- @resource_klass,
367
- context: @context,
368
- data: data,
369
- fields: @fields,
370
- include_directives: @include_directives
371
- )
372
- end
354
+ data = parse_params(params, @resource_klass.creatable_fields(@context))
355
+ @operations.push JSONAPI::Operation.new(:create_resource,
356
+ @resource_klass,
357
+ context: @context,
358
+ data: data,
359
+ fields: @fields,
360
+ include_directives: @include_directives
361
+ )
373
362
  rescue JSONAPI::Exceptions::Error => e
374
363
  @errors.concat(e.errors)
375
364
  end
@@ -567,20 +556,9 @@ module JSONAPI
567
556
  end
568
557
  end
569
558
 
570
- # TODO: Please remove after `updateable_fields` is removed
571
- # :nocov:
572
- def updatable_fields
573
- if @resource_klass.respond_to?(:updateable_fields)
574
- @resource_klass.updateable_fields(@context)
575
- else
576
- @resource_klass.updatable_fields(@context)
577
- end
578
- end
579
- # :nocov:
580
-
581
559
  def parse_add_relationship_operation(verified_params, relationship, parent_key)
582
560
  if relationship.is_a?(JSONAPI::Relationship::ToMany)
583
- @operations.push JSONAPI::Operation.new(:create_to_many_relationship,
561
+ @operations.push JSONAPI::Operation.new(:create_to_many_relationships,
584
562
  resource_klass,
585
563
  context: @context,
586
564
  resource_id: parent_key,
@@ -612,13 +590,15 @@ module JSONAPI
612
590
  fail JSONAPI::Exceptions::ToManySetReplacementForbidden.new
613
591
  end
614
592
  options[:data] = verified_params[:to_many].values[0]
615
- operation_type = :replace_to_many_relationship
593
+ operation_type = :replace_to_many_relationships
616
594
  end
617
595
 
618
596
  @operations.push JSONAPI::Operation.new(operation_type, resource_klass, options)
619
597
  end
620
598
 
621
599
  def parse_single_replace_operation(data, keys, id_key_presence_check_required: true)
600
+ fail JSONAPI::Exceptions::InvalidDataFormat unless data.respond_to?(:each_pair)
601
+
622
602
  fail JSONAPI::Exceptions::MissingKey.new if data[:id].nil?
623
603
 
624
604
  key = data[:id].to_s
@@ -634,38 +614,23 @@ module JSONAPI
634
614
  @resource_klass,
635
615
  context: @context,
636
616
  resource_id: key,
637
- data: parse_params(data, updatable_fields),
617
+ data: parse_params(data, @resource_klass.updatable_fields(@context)),
638
618
  fields: @fields,
639
619
  include_directives: @include_directives
640
620
  )
641
621
  end
642
622
 
643
623
  def parse_replace_operation(data, keys)
644
- if data.is_a?(Array)
645
- fail JSONAPI::Exceptions::CountMismatch if keys.count != data.count
646
-
647
- data.each do |object_params|
648
- parse_single_replace_operation(object_params, keys)
649
- end
650
- else
651
- parse_single_replace_operation(data, [keys],
652
- id_key_presence_check_required: keys.present?)
653
- end
654
-
624
+ parse_single_replace_operation(data, [keys], id_key_presence_check_required: keys.present?)
655
625
  rescue JSONAPI::Exceptions::Error => e
656
626
  @errors.concat(e.errors)
657
627
  end
658
628
 
659
629
  def parse_remove_operation(params)
660
- keys = parse_key_array(params.require(:id))
661
-
662
- keys.each do |key|
663
- @operations.push JSONAPI::Operation.new(:remove_resource,
664
- @resource_klass,
665
- context: @context,
666
- resource_id: key
667
- )
668
- end
630
+ @operations.push JSONAPI::Operation.new(:remove_resource,
631
+ @resource_klass,
632
+ context: @context,
633
+ resource_id: @resource_klass.verify_key(params.require(:id), context))
669
634
  rescue JSONAPI::Exceptions::Error => e
670
635
  @errors.concat(e.errors)
671
636
  end
@@ -678,25 +643,15 @@ module JSONAPI
678
643
  )
679
644
 
680
645
  if relationship.is_a?(JSONAPI::Relationship::ToMany)
646
+ operation_args = operation_base_args.dup
681
647
  keys = params[:to_many].values[0]
682
- keys.each do |key|
683
- operation_args = operation_base_args.dup
684
- operation_args[1] = operation_args[1].merge(associated_key: key)
685
- @operations.push JSONAPI::Operation.new(:remove_to_many_relationship,
686
- *operation_args
687
- )
688
- end
648
+ operation_args[1] = operation_args[1].merge(associated_keys: keys)
649
+ @operations.push JSONAPI::Operation.new(:remove_to_many_relationships, *operation_args)
689
650
  else
690
- @operations.push JSONAPI::Operation.new(:remove_to_one_relationship,
691
- *operation_base_args
692
- )
651
+ @operations.push JSONAPI::Operation.new(:remove_to_one_relationship, *operation_base_args)
693
652
  end
694
653
  end
695
654
 
696
- def parse_key_array(raw)
697
- @resource_klass.verify_keys(raw.split(/,/), context)
698
- end
699
-
700
655
  def format_key(key)
701
656
  @key_formatter.format(key)
702
657
  end