jsonapi-resources 0.8.3 → 0.9.0.beta1

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