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