jsonapi-resources 0.2.0 → 0.3.0.pre1

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.
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