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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.travis.yml +5 -2
- data/Gemfile +3 -1
- data/README.md +52 -13
- data/jsonapi-resources.gemspec +1 -1
- data/lib/jsonapi-resources.rb +1 -0
- data/lib/jsonapi/association.rb +1 -9
- data/lib/jsonapi/error_codes.rb +1 -0
- data/lib/jsonapi/exceptions.rb +9 -5
- data/lib/jsonapi/formatter.rb +9 -18
- data/lib/jsonapi/paginator.rb +4 -15
- data/lib/jsonapi/request.rb +26 -42
- data/lib/jsonapi/resource.rb +35 -45
- data/lib/jsonapi/resource_controller.rb +6 -32
- data/lib/jsonapi/resource_serializer.rb +62 -33
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/routing_ext.rb +4 -4
- data/test/config/database.yml +2 -1
- data/test/controllers/controller_test.rb +200 -160
- data/test/fixtures/active_record.rb +44 -201
- data/test/fixtures/book_comments.yml +11 -0
- data/test/fixtures/books.yml +6 -0
- data/test/fixtures/comments.yml +17 -0
- data/test/fixtures/comments_tags.yml +20 -0
- data/test/fixtures/expense_entries.yml +13 -0
- data/test/fixtures/facts.yml +11 -0
- data/test/fixtures/iso_currencies.yml +17 -0
- data/test/fixtures/people.yml +24 -0
- data/test/fixtures/posts.yml +96 -0
- data/test/fixtures/posts_tags.yml +59 -0
- data/test/fixtures/preferences.yml +18 -0
- data/test/fixtures/sections.yml +8 -0
- data/test/fixtures/tags.yml +39 -0
- data/test/helpers/hash_helpers.rb +0 -7
- data/test/integration/requests/request_test.rb +86 -28
- data/test/integration/routes/routes_test.rb +14 -25
- data/test/test_helper.rb +41 -17
- data/test/unit/jsonapi_request/jsonapi_request_test.rb +152 -0
- data/test/unit/operation/operations_processor_test.rb +13 -2
- data/test/unit/resource/resource_test.rb +68 -13
- data/test/unit/serializer/serializer_test.rb +328 -220
- metadata +33 -6
- data/lib/jsonapi/resource_for.rb +0 -29
data/lib/jsonapi/resource.rb
CHANGED
@@ -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 =
|
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
|
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
|
-
|
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
|
-
|
493
|
-
|
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 =
|
551
|
+
resource_class = Resource.resource_for(self.class.module_path + type_name)
|
562
552
|
if resource_class
|
563
|
-
associated_model =
|
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 =
|
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 =
|
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
|
-
|
43
|
+
key = resource_klass.verify_key(params[resource_klass._primary_key], context)
|
47
44
|
|
48
|
-
|
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(
|
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
|
-
|
147
|
-
|
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
|
-
@
|
30
|
+
@included_objects = {}
|
31
31
|
|
32
32
|
requested_associations = parse_includes(@include)
|
33
33
|
|
34
34
|
process_primary(source, requested_associations)
|
35
35
|
|
36
|
-
|
36
|
+
included_objects = []
|
37
37
|
primary_objects = []
|
38
|
-
@
|
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
|
-
|
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
|
51
|
-
primary_hash[:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 @
|
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
|
228
|
-
|
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] =
|
232
|
-
link_object_hash[:
|
233
|
-
|
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] =
|
244
|
-
link_object_hash[:
|
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
|
-
@
|
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
|
309
|
+
def add_included_object(type, id, object_hash, primary = false)
|
281
310
|
type = format_key(type)
|
282
311
|
|
283
|
-
unless @
|
284
|
-
@
|
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
|
-
@
|
321
|
+
@included_objects[type].store(id, {primary: primary, object_hash: object_hash})
|
293
322
|
end
|
294
323
|
end
|
295
324
|
|