jsonapi-resources 0.2.0 → 0.3.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.travis.yml +5 -2
  4. data/Gemfile +3 -1
  5. data/README.md +52 -13
  6. data/jsonapi-resources.gemspec +1 -1
  7. data/lib/jsonapi-resources.rb +1 -0
  8. data/lib/jsonapi/association.rb +1 -9
  9. data/lib/jsonapi/error_codes.rb +1 -0
  10. data/lib/jsonapi/exceptions.rb +9 -5
  11. data/lib/jsonapi/formatter.rb +9 -18
  12. data/lib/jsonapi/paginator.rb +4 -15
  13. data/lib/jsonapi/request.rb +26 -42
  14. data/lib/jsonapi/resource.rb +35 -45
  15. data/lib/jsonapi/resource_controller.rb +6 -32
  16. data/lib/jsonapi/resource_serializer.rb +62 -33
  17. data/lib/jsonapi/resources/version.rb +1 -1
  18. data/lib/jsonapi/routing_ext.rb +4 -4
  19. data/test/config/database.yml +2 -1
  20. data/test/controllers/controller_test.rb +200 -160
  21. data/test/fixtures/active_record.rb +44 -201
  22. data/test/fixtures/book_comments.yml +11 -0
  23. data/test/fixtures/books.yml +6 -0
  24. data/test/fixtures/comments.yml +17 -0
  25. data/test/fixtures/comments_tags.yml +20 -0
  26. data/test/fixtures/expense_entries.yml +13 -0
  27. data/test/fixtures/facts.yml +11 -0
  28. data/test/fixtures/iso_currencies.yml +17 -0
  29. data/test/fixtures/people.yml +24 -0
  30. data/test/fixtures/posts.yml +96 -0
  31. data/test/fixtures/posts_tags.yml +59 -0
  32. data/test/fixtures/preferences.yml +18 -0
  33. data/test/fixtures/sections.yml +8 -0
  34. data/test/fixtures/tags.yml +39 -0
  35. data/test/helpers/hash_helpers.rb +0 -7
  36. data/test/integration/requests/request_test.rb +86 -28
  37. data/test/integration/routes/routes_test.rb +14 -25
  38. data/test/test_helper.rb +41 -17
  39. data/test/unit/jsonapi_request/jsonapi_request_test.rb +152 -0
  40. data/test/unit/operation/operations_processor_test.rb +13 -2
  41. data/test/unit/resource/resource_test.rb +68 -13
  42. data/test/unit/serializer/serializer_test.rb +328 -220
  43. metadata +33 -6
  44. data/lib/jsonapi/resource_for.rb +0 -29
@@ -1,11 +1,9 @@
1
1
  require 'jsonapi/configuration'
2
- require 'jsonapi/resource_for'
3
2
  require 'jsonapi/association'
4
3
  require 'jsonapi/callbacks'
5
4
 
6
5
  module JSONAPI
7
6
  class Resource
8
- include ResourceFor
9
7
  include Callbacks
10
8
 
11
9
  @@resource_types = {}
@@ -101,6 +99,12 @@ module JSONAPI
101
99
  self.class.fields
102
100
  end
103
101
 
102
+ # Override this on a resource to customize how the associated records
103
+ # are fetched for a model. Particularly helpful for authoriztion.
104
+ def records_for(association_name, options = {})
105
+ model.send association_name
106
+ end
107
+
104
108
  private
105
109
  def save
106
110
  run_callbacks :save do
@@ -123,7 +127,7 @@ module JSONAPI
123
127
  association = self.class._associations[association_type]
124
128
 
125
129
  association_key_values.each do |association_key_value|
126
- related_resource = self.class.resource_for(association.type).find_by_key(association_key_value, context: @context)
130
+ related_resource = Resource.resource_for(association.type).find_by_key(association_key_value, context: @context)
127
131
 
128
132
  # ToDo: Add option to skip relations that already exist instead of returning an error?
129
133
  relation = @model.send(association.type).where(association.primary_key => association_key_value).first
@@ -206,6 +210,15 @@ module JSONAPI
206
210
  @@resource_types[base._type] ||= base.name.demodulize
207
211
  end
208
212
 
213
+ def resource_for(type)
214
+ resource_name = JSONAPI::Resource._resource_name_from_type(type)
215
+ resource = resource_name.safe_constantize if resource_name
216
+ if resource.nil?
217
+ raise NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
218
+ end
219
+ resource
220
+ end
221
+
209
222
  attr_accessor :_attributes, :_associations, :_allowed_filters , :_type, :_paginator
210
223
 
211
224
  def create(context)
@@ -269,13 +282,6 @@ module JSONAPI
269
282
  @_allowed_filters.add(attr.to_sym)
270
283
  end
271
284
 
272
- def key(key)
273
- # :nocov:
274
- warn '[DEPRECATION] `key` is deprecated. Please use `primary_key` instead.'
275
- @_primary_key = key.to_sym
276
- # :nocov:
277
- end
278
-
279
285
  def primary_key(key)
280
286
  @_primary_key = key.to_sym
281
287
  end
@@ -359,20 +365,8 @@ module JSONAPI
359
365
  self.new(model, context)
360
366
  end
361
367
 
362
- def find_by_keys(keys, options = {})
363
- context = options[:context]
364
- _models = records(options).where({_primary_key => keys})
365
-
366
- unless _models.length == keys.length
367
- key = (keys - _models.pluck(_primary_key).map(&:to_s)).first
368
- raise JSONAPI::Exceptions::RecordNotFound.new(key)
369
- end
370
-
371
- _models.map { |model| self.new(model, context) }
372
- end
373
-
374
368
  # Override this method if you want to customize the relation for
375
- # finder methods (find, find_by_key, find_by_keys)
369
+ # finder methods (find, find_by_key)
376
370
  def records(options = {})
377
371
  _model_class
378
372
  end
@@ -403,7 +397,9 @@ module JSONAPI
403
397
 
404
398
  # override to allow for key processing and checking
405
399
  def verify_key(key, context = nil)
406
- return key
400
+ key && Integer(key)
401
+ rescue
402
+ raise JSONAPI::Exceptions::InvalidFieldValue.new(_primary_key, key)
407
403
  end
408
404
 
409
405
  # override to allow for key processing and checking
@@ -453,13 +449,6 @@ module JSONAPI
453
449
  @_model_name ||= self.name.demodulize.sub(/Resource$/, '')
454
450
  end
455
451
 
456
- def _key
457
- # :nocov:
458
- warn '[DEPRECATION] `_key` is deprecated. Please use `_primary_key` instead.'
459
- _primary_key
460
- # :nocov:
461
- end
462
-
463
452
  def _primary_key
464
453
  @_primary_key ||= :id
465
454
  end
@@ -489,17 +478,9 @@ module JSONAPI
489
478
  @_paginator = paginator
490
479
  end
491
480
 
492
- # :nocov:
493
- if RUBY_VERSION >= '2.0'
494
- def _model_class
495
- @model ||= Object.const_get(_model_name.to_s)
496
- end
497
- else
498
- def _model_class
499
- @model ||= _model_name.to_s.safe_constantize
500
- end
481
+ def _model_class
482
+ @model ||= _model_name.to_s.safe_constantize
501
483
  end
502
- # :nocov:
503
484
 
504
485
  def _allowed_filter?(filter)
505
486
  _allowed_filters.include?(filter)
@@ -555,26 +536,35 @@ module JSONAPI
555
536
  @model.method("#{foreign_key}=").call(value)
556
537
  end unless method_defined?("#{foreign_key}=")
557
538
 
539
+ associated_records_method_name = case @_associations[attr]
540
+ when JSONAPI::Association::HasOne then "record_for_#{attr}"
541
+ when JSONAPI::Association::HasMany then "records_for_#{attr}"
542
+ end
543
+
544
+ define_method associated_records_method_name do |options={}|
545
+ records_for(attr, options)
546
+ end unless method_defined?(associated_records_method_name)
547
+
558
548
  if @_associations[attr].is_a?(JSONAPI::Association::HasOne)
559
549
  define_method attr do
560
550
  type_name = self.class._associations[attr].type.to_s
561
- resource_class = self.class.resource_for(self.class.module_path + type_name)
551
+ resource_class = Resource.resource_for(self.class.module_path + type_name)
562
552
  if resource_class
563
- associated_model = @model.send attr
553
+ associated_model = public_send(associated_records_method_name)
564
554
  return associated_model ? resource_class.new(associated_model, @context) : nil
565
555
  end
566
556
  end unless method_defined?(attr)
567
557
  elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
568
558
  define_method attr do |options = {}|
569
559
  type_name = self.class._associations[attr].type.to_s
570
- resource_class = self.class.resource_for(self.class.module_path + type_name)
560
+ resource_class = Resource.resource_for(self.class.module_path + type_name)
571
561
  filters = options.fetch(:filters, {})
572
562
  sort_criteria = options.fetch(:sort_criteria, {})
573
563
  paginator = options.fetch(:paginator, nil)
574
564
 
575
565
  resources = []
576
566
  if resource_class
577
- records = @model.send attr
567
+ records = public_send(associated_records_method_name)
578
568
  records = self.class.apply_filters(records, filters)
579
569
  records = self.class.apply_sort(records, self.class.construct_order_options(sort_criteria))
580
570
  records = self.class.apply_pagination(records, paginator)
@@ -1,4 +1,3 @@
1
- require 'jsonapi/resource_for'
2
1
  require 'jsonapi/resource_serializer'
3
2
  require 'action_controller'
4
3
  require 'jsonapi/exceptions'
@@ -11,8 +10,6 @@ require 'csv'
11
10
 
12
11
  module JSONAPI
13
12
  class ResourceController < ActionController::Base
14
- include ResourceFor
15
-
16
13
  before_filter :ensure_correct_media_type, only: [:create, :update, :create_association, :update_association]
17
14
  before_filter :setup_request
18
15
  after_filter :setup_response
@@ -43,15 +40,11 @@ module JSONAPI
43
40
  key_formatter: key_formatter,
44
41
  route_formatter: route_formatter)
45
42
 
46
- keys = parse_key_array(params[resource_klass._primary_key])
43
+ key = resource_klass.verify_key(params[resource_klass._primary_key], context)
47
44
 
48
- resource_records = if keys.length > 1
49
- resource_klass.find_by_keys(keys, context: context)
50
- else
51
- resource_klass.find_by_key(keys[0], context: context)
52
- end
45
+ resource_record = resource_klass.find_by_key(key, context: context)
53
46
 
54
- render json: serializer.serialize_to_hash(resource_records)
47
+ render json: serializer.serialize_to_hash(resource_record)
55
48
  rescue => e
56
49
  handle_exceptions(e)
57
50
  end
@@ -59,7 +52,7 @@ module JSONAPI
59
52
  def show_association
60
53
  association_type = params[:association]
61
54
 
62
- parent_key = params[resource_klass._as_parent_key]
55
+ parent_key = resource_klass.verify_key(params[resource_klass._as_parent_key], context)
63
56
 
64
57
  parent_resource = resource_klass.find_by_key(parent_key, context: context)
65
58
 
@@ -73,9 +66,7 @@ module JSONAPI
73
66
 
74
67
  render json: serializer.serialize_to_links_hash(parent_resource, association)
75
68
  rescue => e
76
- # :nocov:
77
69
  handle_exceptions(e)
78
- # :nocov:
79
70
  end
80
71
 
81
72
  def create
@@ -143,17 +134,9 @@ module JSONAPI
143
134
  end
144
135
 
145
136
  private
146
- # :nocov:
147
- if RUBY_VERSION >= '2.0'
148
- def resource_klass
149
- @resource_klass ||= Object.const_get resource_klass_name
150
- end
151
- else
152
- def resource_klass
153
- @resource_klass ||= resource_klass_name.safe_constantize
154
- end
137
+ def resource_klass
138
+ @resource_klass ||= resource_klass_name.safe_constantize
155
139
  end
156
- # :nocov:
157
140
 
158
141
  def base_url
159
142
  @base_url ||= request.protocol + request.host_with_port
@@ -168,9 +151,7 @@ module JSONAPI
168
151
  raise JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
169
152
  end
170
153
  rescue => e
171
- # :nocov:
172
154
  handle_exceptions(e)
173
- # :nocov:
174
155
  end
175
156
 
176
157
  def setup_request
@@ -180,9 +161,7 @@ module JSONAPI
180
161
  })
181
162
  render_errors(@request.errors) unless @request.errors.empty?
182
163
  rescue => e
183
- # :nocov:
184
164
  handle_exceptions(e)
185
- # :nocov:
186
165
  end
187
166
 
188
167
  def setup_response
@@ -191,11 +170,6 @@ module JSONAPI
191
170
  end
192
171
  end
193
172
 
194
- def parse_key_array(raw)
195
- keys = raw.nil? || raw.empty? ? [] : raw.split(',')
196
- resource_klass.verify_keys(keys, context)
197
- end
198
-
199
173
  # override to set context
200
174
  def context
201
175
  {}
@@ -27,28 +27,28 @@ module JSONAPI
27
27
  def serialize_to_hash(source)
28
28
  is_resource_collection = source.respond_to?(:to_ary)
29
29
 
30
- @linked_objects = {}
30
+ @included_objects = {}
31
31
 
32
32
  requested_associations = parse_includes(@include)
33
33
 
34
34
  process_primary(source, requested_associations)
35
35
 
36
- linked_objects = []
36
+ included_objects = []
37
37
  primary_objects = []
38
- @linked_objects.each_value do |objects|
38
+ @included_objects.each_value do |objects|
39
39
  objects.each_value do |object|
40
40
  if object[:primary]
41
41
  primary_objects.push(object[:object_hash])
42
42
  else
43
- linked_objects.push(object[:object_hash])
43
+ included_objects.push(object[:object_hash])
44
44
  end
45
45
  end
46
46
  end
47
47
 
48
48
  primary_hash = {data: is_resource_collection ? primary_objects : primary_objects[0]}
49
49
 
50
- if linked_objects.size > 0
51
- primary_hash[:linked] = linked_objects
50
+ if included_objects.size > 0
51
+ primary_hash[:included] = included_objects
52
52
  else
53
53
  primary_hash
54
54
  end
@@ -56,7 +56,19 @@ module JSONAPI
56
56
  end
57
57
 
58
58
  def serialize_to_links_hash(source, requested_association)
59
- {data: link_object(source, requested_association, true)}
59
+ if requested_association.is_a?(JSONAPI::Association::HasOne)
60
+ data = has_one_linkage(source, requested_association)
61
+ else
62
+ data = has_many_linkage(source, requested_association)
63
+ end
64
+
65
+ {
66
+ links: {
67
+ self: self_link(source, requested_association),
68
+ related: related_link(source, requested_association)
69
+ },
70
+ data: data
71
+ }
60
72
  end
61
73
 
62
74
  private
@@ -95,7 +107,7 @@ module JSONAPI
95
107
  set_primary(@primary_class_name, id)
96
108
  end
97
109
 
98
- add_linked_object(@primary_class_name, id, object_hash(resource, requested_associations), true)
110
+ add_included_object(@primary_class_name, id, object_hash(resource, requested_associations), true)
99
111
  end
100
112
  else
101
113
  resource = source
@@ -105,7 +117,7 @@ module JSONAPI
105
117
  # set_primary(@primary_class_name, id)
106
118
  # end
107
119
 
108
- add_linked_object(@primary_class_name, id, object_hash(source, requested_associations), true)
120
+ add_included_object(@primary_class_name, id, object_hash(source, requested_associations), true)
109
121
  end
110
122
  end
111
123
 
@@ -185,7 +197,7 @@ module JSONAPI
185
197
  id = resource.id
186
198
  associations_only = already_serialized?(type, id)
187
199
  if include_linkage && !associations_only
188
- add_linked_object(type, id, object_hash(resource, ia[:include_related]))
200
+ add_included_object(type, id, object_hash(resource, ia[:include_related]))
189
201
  elsif include_linked_children || associations_only
190
202
  links_hash(resource, ia[:include_related])
191
203
  end
@@ -196,7 +208,7 @@ module JSONAPI
196
208
  id = resource.id
197
209
  associations_only = already_serialized?(type, id)
198
210
  if include_linkage && !associations_only
199
- add_linked_object(type, id, object_hash(resource, ia[:include_related]))
211
+ add_included_object(type, id, object_hash(resource, ia[:include_related]))
200
212
  elsif include_linked_children || associations_only
201
213
  links_hash(resource, ia[:include_related])
202
214
  end
@@ -217,36 +229,53 @@ module JSONAPI
217
229
 
218
230
  def already_serialized?(type, id)
219
231
  type = format_key(type)
220
- return @linked_objects.key?(type) && @linked_objects[type].key?(id)
232
+ return @included_objects.key?(type) && @included_objects[type].key?(id)
221
233
  end
222
234
 
223
235
  def format_route(route)
224
236
  @route_formatter.format(route.to_s)
225
237
  end
226
238
 
227
- def link_object_has_one(source, association)
228
- route = association.name
239
+ def self_link(source, association)
240
+ "#{self_href(source)}/links/#{format_route(association.name)}"
241
+ end
242
+
243
+ def related_link(source, association)
244
+ "#{self_href(source)}/#{format_route(association.name)}"
245
+ end
246
+
247
+ def has_one_linkage(source, association)
248
+ linkage = {}
249
+ linkage_id = foreign_key_value(source, association)
250
+ if linkage_id
251
+ linkage[:type] = format_route(association.type)
252
+ linkage[:id] = linkage_id
253
+ end
254
+ linkage
255
+ end
256
+
257
+ def has_many_linkage(source, association)
258
+ linkage = []
259
+ linkage_ids = foreign_key_value(source, association)
260
+ linkage_ids.each do |linkage_id|
261
+ linkage.append({type: format_route(association.type), id: linkage_id})
262
+ end
263
+ linkage
264
+ end
229
265
 
266
+ def link_object_has_one(source, association)
230
267
  link_object_hash = {}
231
- link_object_hash[:self] = "#{self_href(source)}/links/#{format_route(route)}"
232
- link_object_hash[:resource] = "#{self_href(source)}/#{format_route(route)}"
233
- # ToDo: Get correct formatting figured out
234
- link_object_hash[:type] = format_route(association.type)
235
- link_object_hash[:id] = foreign_key_value(source, association)
268
+ link_object_hash[:self] = self_link(source, association)
269
+ link_object_hash[:related] = related_link(source, association)
270
+ link_object_hash[:linkage] = has_one_linkage(source, association)
236
271
  link_object_hash
237
272
  end
238
273
 
239
274
  def link_object_has_many(source, association, include_linkage)
240
- route = association.name
241
-
242
275
  link_object_hash = {}
243
- link_object_hash[:self] = "#{self_href(source)}/links/#{format_route(route)}"
244
- link_object_hash[:resource] = "#{self_href(source)}/#{format_route(route)}"
245
- if include_linkage
246
- # ToDo: Get correct formatting figured out
247
- link_object_hash[:type] = format_route(association.type)
248
- link_object_hash[:ids] = foreign_key_value(source, association)
249
- end
276
+ link_object_hash[:self] = self_link(source, association)
277
+ link_object_hash[:related] = related_link(source, association)
278
+ link_object_hash[:linkage] = has_many_linkage(source, association) if include_linkage
250
279
  link_object_hash
251
280
  end
252
281
 
@@ -273,15 +302,15 @@ module JSONAPI
273
302
  # Sets that an object should be included in the primary document of the response.
274
303
  def set_primary(type, id)
275
304
  type = format_key(type)
276
- @linked_objects[type][id][:primary] = true
305
+ @included_objects[type][id][:primary] = true
277
306
  end
278
307
 
279
308
  # Collects the hashes for all objects processed by the serializer
280
- def add_linked_object(type, id, object_hash, primary = false)
309
+ def add_included_object(type, id, object_hash, primary = false)
281
310
  type = format_key(type)
282
311
 
283
- unless @linked_objects.key?(type)
284
- @linked_objects[type] = {}
312
+ unless @included_objects.key?(type)
313
+ @included_objects[type] = {}
285
314
  end
286
315
 
287
316
  if already_serialized?(type, id)
@@ -289,7 +318,7 @@ module JSONAPI
289
318
  set_primary(type, id)
290
319
  end
291
320
  else
292
- @linked_objects[type].store(id, {primary: primary, object_hash: object_hash})
321
+ @included_objects[type].store(id, {primary: primary, object_hash: object_hash})
293
322
  end
294
323
  end
295
324